DEV Community

Axios vs. Fetch: Which Fetch Wrapper Should I Choose in 2025?

suhaotian on March 10, 2025

It's 2025, and Axios has supported fetch since v1.7, making it just another fetch wrapper. The Axios package is 35.6KB/Gzip 13.5KB—too big as a ...
Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Why people complicate themselves? The fetch() function is all you need 99% of the time. 1% might need upload/download progress, and that's about it. Interceptors are the worst monster ever. I immediately reject any wrapper with this concept when I can simply do a data-fetching function of my own in a couple lines of code.

Collapse
 
suhaotian profile image
suhaotian • Edited

Thank you for the comment!

Fetch for a simple use case is okay.

But there are different use cases, which is why people choose a fetch wrapper or build their own.

Because A fetch wrapper is used for:

  1. Customization – Easily add headers, tokens, or base URL.
  2. Central Error handling – Manage errors in one place for your app.
  3. Reusability – Centralize API logic to avoid repeating code.
  4. Abstraction – Simplifies handling requests and responses.
  5. Extra features – Supports things like error retry, caching or custom your own plugins.
Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Customization

export function myFetch(url, options) {
  options.headers = {
    'Authorization': `Bearer ${token}`
  };
  return fetch(new URL(url, 'http://example.com'), options);
}
Enter fullscreen mode Exit fullscreen mode

Yet somehow people convince themselves that interceptors are "the way to go". Compare this code with the code require for an interceptor.

Error handling

Rookie mistake. Error handling is only done at the point where the user can be informed. Middle-layer error handling is a rookie mistake.

Reusability

The myFetch function is reusable.

Abstraction

All packages abstract the response incorrectly because they all do:

if (!response.ok) {
  throw new Error('Response failed.');
}
Enter fullscreen mode Exit fullscreen mode

This anit-pattern is exchanging branching for catching.

Extra Features

The minority of the people require extra features. I agree, they are sometimes needed.


So all in all, they are all superflous and badly done.

Thread Thread
 
suhaotian profile image
suhaotian • Edited

Wow, indeed it's simple!
I did this before—I have my own fetch wrapper in the project codebase.

After I tried Axios, it was so good that I never used my own fetch wrapper like myFetch(url, options), you need add qs module to support nested query encoding which is another ~11KB in size.

But one year ago, I encountered some issues running Axios in Next.js middleware and Cloudflare Worker runtime. that's why I wrote my axios-like API fetch wrapper, and share it with people, and fixing issues, add unit testing, make it stronger and stable.

Collapse
 
shash7 profile image
Shash

This is like saying use Date() instead of momentjs(or any other momentjs variations).

Collapse
 
suhaotian profile image
suhaotian • Edited

Yes, for simple use cases, fetch is sufficient, just like using new Date(). However, for date parsing, formatting, and validation, we need Date() wrapper libraries such as dayjs, date-fns, or moment.js.

That's why a fetch wrapper is beneficial—whether from the community or one we develop ourselves.

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

Using cloc, xior is over 1,700 LOC:

github.com/AlDanial/cloc v 2.05  T=0.12 s (164.8 files/s, 17136.3 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
TypeScript                      20            190            153           1737
-------------------------------------------------------------------------------
SUM:                            20            190            153           1737
-------------------------------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

How much of this is needed on average?

This is the same story for axios, ky, etc.

Yes, a wrapper is beneficial. Just not these wrappers.

Have a look at this code from xior's README:

http.interceptors.response.use(
  (result) => {
    const { data, request: config, response: originalResponse } = result;
    return result;
  },
  async (error) => {
    if (error instanceof TypeError) {
      console.log(`Request error:`, error);
    }
    if (error?.response?.status === 401) {
      localStorage.removeItem('REQUEST_TOKEN');
    }
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

This is a response interceptor. Look how evident the anti-pattern is here: The NPM package exchanged, using an IF statement, a non-OK response status for a thrown error. This has forced the above example, which is the consumer's code to invert this: The consumer now has to use the callback, examin the error using another IF, just to do what you should have done in the first place: Branch with response.ok or response.status.

This is so evident. How come people keep defending what has no defense? This is madness.

Thread Thread
 
suhaotian profile image
suhaotian • Edited

Yes, Xior includes typescript types, plugins and many other features, which is why there are so many lines of code. And even more lines with the unit tests if you count the tests/ folder.

The response interceptor example code shows people what properties they can get from the function.

I appreciate your comment on this post and the advice. Sorry that the fetch wrapper is not working for you.

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

I just counted the LOC in /src using the command perl ..\cloc-dev.pl .\src.

The best NPM packages don't add unnecessary overhead and are focused on solving one problem. The main problem with all popular fetch wrappers is the throw-instead-of-branching anti-pattern, which creates overhead where none should exist. The next big problem is the interceptors, where consumers of the package end up writing more code than if no wrapper was used.

Thread Thread
 
suhaotian profile image
suhaotian

What's meaning of throw-instead-of-branching anti-pattern, can you give examples?

For interceptors, it's idea from Axios which I think is nice, you can use many request or response interceptors:

import axios from 'xior';

const http = axios.create({ ... });

http.interceptors.request.use((req) => {
   // ... do something 1
   return req;
})
http.interceptors.request.use((req) => {
   // ... do something 2
   return req;
})

http.interceptors.response.use((response) => {
   // ... do something with response 1
   return response;
})
http.interceptors.response.use((response) => {
   // ... do something with response 2
   return response;
})
Enter fullscreen mode Exit fullscreen mode

And custom our own plugin with http.plugins.use:

http.plugins.use(function logPlugin(adapter, instance) {
  return async (config) => {
    const start = Date.now();
    const res = await adapter(config);
    console.log('%s %s %s take %sms', config.method, config.url, res.status, Date.now() - start);
    return res;
  };
});
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

I explained it in another response: The package uses an IF statement to throw an error. Then the consumer is forced to revert what the package did, either within a callback, or in a catch block and then use an extra IF to determine things like in the example I took from xior's README, which was to test for a 401 response to delete the token.

A good wrapper would instead forward the ok and status properties and let the user decide what is wanted in special cases, with just one IF statement, not 2.

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas • Edited

Also, just because axios does it doesn't mean it is OK. It is my professional opinion that axios, ky and xior all do this incorrectly, no matter how many years this has gone unnoticed.

Thread Thread
 
suhaotian profile image
suhaotian

You can actually access ok and status from the response:

import axios from 'xior';

const http = axios.create();

http.get('/').then(result => {
   const { response } = result; // the original fetch response object
   console.log(response.ok, response.status, response.statusText);
});
Enter fullscreen mode Exit fullscreen mode

For the "if throw" problem, good catch! Do you think adding an option like a checkResponse function to let the user determine whether an error should be thrown is a good choice? Or you talking something similar like JavaScript's Safe Assignment Operator Proposal?

Thread Thread
 
webjose profile image
José Pablo Ramírez Vargas

I don't propose anything except discarding all these packages as their flaws are plural, not singular. I created my own wrapper library that does the job properly, and for the cherry on top, allows you to type the response body for all possible HTTP status codes. You can call it "doctor fetch". [dr-fetch](github.com/WJSoftware/dr-fetch]. This wrapper takes a completely different approach to the problem.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Would you like to give any reasons with logical argumentation? Because I don't follow.

Collapse
 
link2twenty profile image
Andrew Bone • Edited

Axios existed before fetch was a thing, now that fetch is a thing why load a bunch of extra code to do the same thing?

When there is a new spec that can do all the things moment can do devs should stop using moment. There is already DateTimeFormat and RelativeTimeFormat which get you part way there.

Collapse
 
himanshu_code profile image
Himanshu Sorathiya

It's 2025 and still people thinking of using something else and doing comparison 🙄.
I'm a newbie ( 1 year old ), and still knows that no matter of others fetch is all you need.