Why Should I Use Axios Instead of Fetch? Practical Comparison
Problem
I saw a Reddit post where a developer asked: “I just have a tiny fetch wrapper and never did I think it needed anything more. Why does everyone use axios instead of fetch?”
The top comment had 235 upvotes: “Your wrapper covers like 80% of what most people need honestly.”
That made me wonder: what’s in the other 20%? And why do so many projects still install axios?
What I Started With
My fetch wrapper looked like this:
async function request(url, options = {}) { const response = await fetch(url, { headers: { 'Content-Type': 'application/json', ...options.headers }, ...options })
if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) }
return response.json()}This worked fine for simple GET and POST requests. But then my project requirements grew.
Where Fetch Started Hurting
Problem 1: Adding Auth to Every Request
When I needed to add authentication headers to every request, I had to modify my wrapper:
async function request(url, options = {}) { const token = localStorage.getItem('token')
const response = await fetch(url, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, ...options.headers }, ...options })
// ... rest of the code}But then I needed token refresh logic for 401 responses. My wrapper kept growing.
Problem 2: Timeouts
I needed request timeouts. Fetch doesn’t have built-in timeout support:
async function fetchWithTimeout(url, timeout = 5000) { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout)
try { const response = await fetch(url, { signal: controller.signal }) clearTimeout(timeoutId) return response } catch (error) { clearTimeout(timeoutId) if (error.name === 'AbortError') { throw new Error('Request timed out') } throw error }}This works, but it’s boilerplate I had to write myself.
Problem 3: Error Handling
Fetch only rejects on network errors. HTTP errors (4xx, 5xx) don’t throw:
fetch('/api/data') .then(response => { if (!response.ok) { // I have to manually check and throw throw new Error(response.statusText) } return response.json() }) .catch(error => { // This catches both network errors AND my manual throws // But I lose the response data console.log(error.message) })I had to check response.ok every time. And I couldn’t access the response body when errors happened.
Problem 4: Upload Progress
I needed to show upload progress for file uploads. Fetch doesn’t support this.
I found workarounds using streams, but they were complex and browser support was inconsistent.
What Axios Gives Me
I installed axios and rewrote my setup:
import axios from 'axios'
const api = axios.create({ baseURL: '/api', timeout: 10000, headers: { 'Content-Type': 'application/json' }})Interceptors: The Killer Feature
This is where axios shines. I added auth and token refresh in one place:
// Add auth token to every requestapi.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config})
// Handle 401 responses globallyapi.interceptors.response.use( response => response, async error => { if (error.response?.status === 401) { const newToken = await refreshToken() error.config.headers.Authorization = `Bearer ${newToken}` return axios.request(error.config) // Retry with new token } return Promise.reject(error) })Every request now automatically includes auth. And 401 errors trigger token refresh automatically.
Automatic JSON Parsing
// Fetch requires manual parsingconst response = await fetch('/api/data')const data = await response.json()
// Axios does it automaticallyconst { data } = await api.get('/api/data')One line instead of two. Small, but it removes a common mistake (forgetting to call .json()).
Better Error Object
api.get('/api/data') .catch(error => { // error.response contains everything console.log(error.response.status) // 404 console.log(error.response.data) // { message: 'Not found' } console.log(error.response.headers) // response headers console.log(error.config) // the request config })I can access status, body, and headers from the error. Fetch gives me none of this.
Upload Progress
axios.post('/upload', formData, { onUploadProgress: progress => { const percent = Math.round((progress.loaded / progress.total) * 100) console.log(`${percent}% uploaded`) }})Built-in. No workaround needed.
Comparison Table
| Feature | Fetch | Axios ||----------------------|-----------------|----------------------|| Bundle size | 0KB (native) | ~12KB || JSON parsing | Manual | Automatic || Timeout | AbortController | Built-in option || Upload progress | No | Built-in || Interceptors | No | Yes || Error object | Limited | Full response data || Request retry | Manual | Easy with interceptors|| Browser support | Modern browsers | All (including IE11) |When to Choose Each
Choose Fetch When
- Small to medium projects
- Bundle size is critical
- Only need basic GET/POST
- Modern browsers only
- Want zero dependencies
Choose Axios When
- Enterprise app with auth interceptors
- Need automatic transforms
- Require upload progress
- Working with legacy browsers
- Want consistent error handling
- Team already familiar with axios
Common Mistakes
Mistake 1: Over-Engineering Simple Apps
I saw projects with axios for a single GET request:
import axios from 'axios'
// This could just be fetchaxios.get('/api/status') .then(({ data }) => console.log(data))If you’re making one request without auth, use fetch. No need for 12KB of library.
Mistake 2: Under-Engineering Complex Apps
I also saw developers building fetch wrappers that grew into 100-line monsters:
// This wrapper is now more complex than axios// And it still doesn't have interceptorsasync function request(url, options) { // timeout handling... // auth handling... // error handling... // retry logic... // ... 50 more lines}If you need interceptors, timeouts, and error handling, use axios. Don’t build a mediocre version yourself.
Mistake 3: Not Understanding Error Behavior
The biggest confusion I see: fetch doesn’t reject on HTTP errors.
// Fetch: 404 is NOT an errorfetch('/api/notfound') .then(response => { console.log(response.ok) // false console.log(response.status) // 404 // But the promise RESOLVED, not rejected })
// Axios: 404 IS an erroraxios.get('/api/notfound') .catch(error => { console.log(error.response.status) // 404 // The promise was REJECTED })This difference causes subtle bugs when developers expect fetch to throw on 404.
What the Reddit Discussion Revealed
The Reddit thread had a clear consensus:
-
“Axios made sense when fetch was inconsistent across browsers but that’s not really a thing anymore” - Browser compatibility is no longer the primary reason.
-
“Fetch did not have headers object so we used axios. But this was years ago” - Modern fetch has closed most of the feature gap.
-
The main reason now: interceptors for auth and request/response transformation.
-
A simple fetch wrapper covers 80% of use cases. Axios is for the other 20%.
Summary
I use fetch for simple projects. My tiny wrapper handles JSON parsing and basic error checking. That’s enough for most cases.
But for enterprise apps with authentication, token refresh, timeouts, and upload progress, I switch to axios. The interceptors alone save me from writing repetitive auth logic in every request.
The key point: don’t add axios just because it’s popular. Add it when you actually need the features. And don’t build a complex fetch wrapper when axios already solves those problems.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments