Skip to content

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:

fetchWrapper.js
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:

authWrapper.js
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:

timeoutFetch.js
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:

fetchErrorHandling.js
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:

axiosSetup.js
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:

interceptors.js
// Add auth token to every request
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Handle 401 responses globally
api.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

jsonParsing.js
// Fetch requires manual parsing
const response = await fetch('/api/data')
const data = await response.json()
// Axios does it automatically
const { 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

axiosError.js
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

uploadProgress.js
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

axios_vs_fetch.txt
| 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:

overkill.js
import axios from 'axios'
// This could just be fetch
axios.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:

complexWrapper.js
// This wrapper is now more complex than axios
// And it still doesn't have interceptors
async 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.

errorBehavior.js
// Fetch: 404 is NOT an error
fetch('/api/notfound')
.then(response => {
console.log(response.ok) // false
console.log(response.status) // 404
// But the promise RESOLVED, not rejected
})
// Axios: 404 IS an error
axios.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:

  1. “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.

  2. “Fetch did not have headers object so we used axios. But this was years ago” - Modern fetch has closed most of the feature gap.

  3. The main reason now: interceptors for auth and request/response transformation.

  4. 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