Geonode logo
Geonode Team

Geonode Team

Updated: September 1, 2026

Published: 2026-09-02

Axios vs Fetch

One difference matters more than all the others combined: fetch does not reject on HTTP errors. A 404 or a 500 resolves successfully and your code carries on. Everything else — bundle size, syntax, interceptors — is preference. That one is a bug generator, and it is documented behaviour rather than a quirk. This guide covers what each tool actually does per its own documentation, the boilerplate you write to make fetch behave, and the proxy gap in Node that catches people out.

Two ways to make an HTTP request in JavaScript, argued about endlessly, and the argument is usually conducted on the least important axis.

Bundle size, syntax elegance, whether a dependency is justified — these are preferences, and reasonable people land in different places. One difference is not a preference, and it produces real bugs in production code: fetch does not reject its promise when the server returns an error status. A 404 resolves. A 500 resolves. Your .catch() never runs and the code proceeds as though everything worked.

That is documented behaviour rather than a quirk, and it is the thing to understand before anything else.

We are Geonode and we sell proxies, so the on-topic note goes here: there is a genuine and under-documented difference between these two around proxy support in Node, and it catches people out regularly. Native fetch in Node does not pick up the standard proxy environment variables, which surprises almost everyone who assumes it behaves like curl. That has its own section, and if you are not routing requests through a proxy you can skip it entirely — most people should.

A small housekeeping note, since it affects anyone following older links: Axios's documentation has moved. axios-http.com now redirects to axios.rest. Bookmarks and Stack Overflow answers pointing at the old domain still work through the redirect, but the canonical location has changed.

Everything below about behaviour comes from MDN and Axios's own documentation rather than from anyone's recollection.

The Difference That Causes Real Bugs

Start here, because it is the only difference that is not a matter of taste.

What MDN Says

Directly from the documentation:

"A fetch() promise only rejects when the request fails, for example, because of a badly-formed request URL or a network error. A fetch() promise does not reject if the server responds with HTTP status codes that indicate errors (404, 504, etc.). Instead, a then() handler must check the Response.ok and/or Response.status properties."

The emphasis on does not is MDN's own.

What That Means in Practice

// Looks correct. Is not.
try {
  const res = await fetch('/api/user/999');
  const user = await res.json();
  showUser(user);          // runs on a 404
} catch (err) {
  showError(err);          // never runs on a 404
}

The server returned 404 with an error body. fetch resolved happily. res.json() parsed the error object. showUser received something that is not a user, and the failure surfaces later, somewhere else, as a confusing error about an undefined property.

The correct version:

try {
  const res = await fetch('/api/user/999');
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  const user = await res.json();
  showUser(user);
} catch (err) {
  showError(err);
}

Three extra lines, and they are required in every single request you write.

How Axios Behaves

Axios rejects on status codes outside the 2xx range by default. The equivalent code needs no status check:

try {
  const { data } = await axios.get('/api/user/999');
  showUser(data);
} catch (err) {
  showError(err);          // runs on a 404
}

Which Behaviour Is Correct

Both are defensible, and the disagreement is philosophical.

fetch takes the position that the transfer succeeded — the server was reached, it answered, the response arrived intact. A 404 is a valid answer to a question, not a failure to ask it. Rejecting would conflate transport failure with application semantics. This is, incidentally, exactly curl's position too, where a 404 also produces a zero exit code.

Axios takes the position that most callers treat a 4xx or 5xx as a failure, so it should behave like one.

The practical reality is that fetch's model is more correct and more error-prone, because it requires discipline on every call and nothing warns you when the discipline lapses.

What Fetch Is and What It Costs

The standard, with its advantages and its omissions.

The Advantages

It is built in. No dependency, no install, no bundle cost, no supply chain to audit. In browsers and in modern Node, it is simply there.

It is a standard. Specified rather than maintained by a project that might change direction, be abandoned, or introduce a breaking change on its own schedule.

It is the foundation. Many higher-level libraries are wrappers around it, so understanding fetch means understanding what they do.

It handles streaming well. Response.body is a readable stream, which makes progressive processing of large responses natural.

What It Does Not Do

These are the gaps you fill yourself, and their number is the actual argument for Axios.

No automatic JSON. You call .json(), which is another await and another place to fail if the response is not JSON — an HTML error page, for instance, which is exactly what you get from a misconfigured server.

No status rejection. Covered above.

No timeout by default. A fetch can hang indefinitely. AbortSignal.timeout() provides one in modern environments, but it is opt-in and easy to forget — which matters most in exactly the situations where a hang is worst.

No interceptors. Nowhere to attach an auth token, a correlation ID or logging centrally. Either every call repeats it or you write a wrapper, and writing a wrapper is how people accidentally build their own worse Axios.

No upload progress. Download progress is possible via the response stream; upload progress is not straightforward.

No automatic request body serialisation. You call JSON.stringify and set the content type yourself, every time.

No proxy configuration in Node. Its own section below.

The Honest Summary

fetch is a well-designed low-level primitive. Its omissions are deliberate — a standard should be minimal and unopinionated.

The question is not whether it is good. It is whether you want to implement the missing layer yourself, and whether the version you implement will be better than one maintained by a project with a large user base finding its edge cases.

What Axios Gives You

From its own documentation, the feature list is the argument.

Promise-based HTTP client with a consistent interface across environments, shipping separate browser and Node bundles.

Interceptors for request and response, which is the single most valuable feature and the one hardest to replicate cleanly. Attach an auth header once, handle 401 refresh once, add logging once — and every request in the application inherits it.

Automatic JSON handling in both directions. Request bodies are serialised and the content type is set; responses are parsed into response.data.

Error handling with rejection on non-2xx statuses.

Timeout configuration, described in the documentation as preventing indefinite request hangs. One config value rather than an abort controller per call.

Cancellation of in-flight requests.

Progress tracking for uploads as well as downloads, which fetch does not offer straightforwardly.

XSRF protection built in.

File posting and multipart form data handled for you.

Rate limiting and request throttling.

Instances with defaults, so a configured client with a base URL, headers and timeout can be created once and imported everywhere.

The Costs

A dependency. Something to install, keep updated, and audit. In a security-conscious environment that is a real cost rather than a theoretical one.

Bundle size. Meaningful for a small frontend, negligible for a large application or anything server-side.

Another abstraction to learn, and one that occasionally behaves differently from the platform underneath in ways that surprise you.

The Fair Framing

Axios is roughly what most people end up building on top of fetch when they need these behaviours — except already written, already debugged, and already handling the cases you have not thought of.

If you need none of them, it is a dependency for nothing.

Side by Side

fetchAxios
InstallationBuilt innpm install
Rejects on 404/500NoYes
JSON parsingManual .json()Automatic
Request body serialisationManualAutomatic
TimeoutAbortSignal.timeout()Config option
InterceptorsNoYes
Upload progressNot straightforwardYes
Download progressVia response streamYes
CancellationAbortControllerBuilt in
XSRF protectionManualBuilt in
Instances with defaultsNoYes
Proxy config in NodeNoYes
StreamingExcellentMore limited
Bundle costZeroSmall but non-zero

The Same Request, Both Ways

// fetch, written correctly
const res = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify({ name: 'Alice' }),
  signal: AbortSignal.timeout(5000)
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();

// Axios
const { data } = await axios.post(
  'https://api.example.com/users',
  { name: 'Alice' },
  { headers: { Authorization: `Bearer ${token}` }, timeout: 5000 }
);

Both are correct. The fetch version is eleven lines to Axios's five, and the difference is entirely made up of things you must remember on every call rather than configure once.

The Two Rows That Decide It

Rejects on 404/500 and proxy config in Node are the only rows where one tool cannot straightforwardly do what the other does. The rest is boilerplate, and boilerplate is a cost rather than an impossibility.

The Boilerplate Tax

The real comparison is not between one fetch call and one axios call. It is between a codebase using each.

What Everyone Eventually Writes

After the third or fourth place you repeat the status check, the timeout and the JSON parsing, you write this:

async function request(url, options = {}) {
  const res = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...options.headers
    },
    signal: options.signal ?? AbortSignal.timeout(options.timeout ?? 10000)
  });
  if (!res.ok) {
    const body = await res.text();
    throw new HttpError(res.status, body);
  }
  return res.status === 204 ? null : res.json();
}

That is a reasonable and quite good wrapper. It is also, unmistakably, a small Axios.

What the Wrapper Will Not Handle Until Someone Notices

Retry with backoff. Token refresh on 401 without a request storm when several calls fail at once. Requests that return HTML instead of JSON. 204 responses with no body. Cancellation propagated through nested calls. Upload progress. Content types other than JSON. Correlation IDs for tracing.

Each is a small addition. Collectively they are a library, and the version you write will be less tested than the one thousands of people are already using.

When Writing It Yourself Is Right

When you need very little of it. A handful of GET requests to one API. The wrapper above is thirty lines and you own it completely.

When bundle size genuinely matters. A performance-critical page where every kilobyte is contested.

When dependencies are expensive. Environments where every package requires review.

When you want to understand the platform. A legitimate reason, and the understanding transfers.

When It Is Not

When you are on your third wrapper iteration, when different parts of the codebase have different wrappers, or when you are adding retry logic. At that point you are maintaining a library as a side project, and the dependency you avoided is cheaper than the one you built.

Bundle Size and the Node Question

The two contextual factors that shift the answer.

In the Browser

Axios adds weight to your bundle. Whether that matters depends entirely on what you are building.

A landing page or a widget where load time is the product — use fetch. Every kilobyte is real, and the request patterns are usually simple enough that the boilerplate is trivial.

A large application already shipping a framework and a component library — the marginal cost is noise, and the interceptor support is worth considerably more than the bytes.

The honest position is that bundle size is the argument people reach for when they want a technical reason for an aesthetic preference. It is real, and it is decisive far less often than it is cited.

In Node

Different calculation entirely. Bundle size is irrelevant on a server, so the main argument against Axios evaporates.

Native fetch is available in modern Node and works well. But server-side code tends to need precisely the things fetch omits: timeouts on everything, retry with backoff, centralised auth handling, structured logging of outbound calls, and — as the next section covers — proxy configuration.

So the balance tips toward Axios on the server more than in the browser, which is the opposite of how the argument is usually conducted.

The Compromise Nobody Mentions

You can use both. fetch for simple calls, Axios where you need the machinery. Nothing forbids it and the consistency argument is weaker than it sounds.

What genuinely causes problems is three different homegrown wrappers in one codebase, each handling errors slightly differently. That is worse than either library used consistently, and it is where a surprising number of projects end up.

Proxies in Both

Our territory, and the source of a genuinely confusing afternoon for a lot of developers.

The Short Version

Native fetch in Node does not read the standard proxy environment variables. Setting HTTP_PROXY and HTTPS_PROXY does nothing, unlike curl, unlike most HTTP libraries, and unlike what almost everyone expects.

Node's global fetch is built on undici, and routing through a proxy requires supplying a dispatcher explicitly:

import { ProxyAgent, setGlobalDispatcher } from 'undici';

setGlobalDispatcher(new ProxyAgent('http://user:pass@proxy.example.com:8080'));

// now fetch goes through the proxy
const res = await fetch('https://example.com');

Or per request, by passing a dispatcher option.

Axios in Node

Axios has a proxy configuration option:

const res = await axios.get('https://example.com', {
  proxy: {
    protocol: 'http',
    host: 'proxy.example.com',
    port: 8080,
    auth: { username: 'user', password: 'pass' }
  }
});

For SOCKS proxies, or for finer control, the usual approach is an agent library passed as httpAgent and httpsAgent.

In the Browser, Neither Can

Worth stating because it saves time. JavaScript in a browser cannot set a proxy. The browser uses whatever the system or an extension has configured, and no library changes that. Axios's proxy option is a Node feature.

If you need proxied requests from browser-based code, the request has to go through a server you control.

The Debugging Tell

If a proxy "is not working" in Node, check which client is making the request before checking anything else. Code that works with Axios and fails with fetch — or vice versa — is almost always this, and it is invisible because neither errors. The request simply goes direct.

Verify by requesting an address-reporting endpoint through your configured client and confirming the address returned is the proxy's.

And the Part Against Our Own Interest

Most server-side HTTP calls need no proxy at all. Calling an API you have credentials for, from a server permitted to reach it, requires nothing extra — a proxy adds latency, a failure point and a bill. Proxies earn their place for geographic checking and for collection at volume where per-address rate limits bind. Outside that, the plain client is the better client.

Which to Choose

A decision list rather than a verdict.

Use fetch when

Bundle size is genuinely critical. Landing pages, widgets, embedded scripts.

Your requests are simple. A few GETs, minimal error handling, no shared auth.

You cannot add dependencies, or every package requires review.

You are working with streams. fetch's streaming model is better and this is a real technical advantage rather than a preference.

You want to learn the platform. The knowledge transfers; Axios-specific knowledge does not.

Use Axios when

You need interceptors. Centralised auth, token refresh, logging, correlation IDs. This is the strongest single reason and it has no clean fetch equivalent.

You are on the server. Bundle size does not apply and the missing features are exactly what server code needs.

You need upload progress, which fetch does not make straightforward.

You are making many varied requests across a large codebase and want consistent behaviour without maintaining a wrapper.

You need proxy configuration and would rather use a documented option than assemble a dispatcher.

Whichever You Pick

Always set a timeout. AbortSignal.timeout() or Axios's timeout option. A request without one can hang indefinitely, and this is the most common reliability defect in JavaScript HTTP code.

Always check status with fetch. Every call, without exception. res.ok is two words and the absence of them is the bug this article opened with.

Centralise it. One wrapper or one configured Axios instance. Three inconsistent approaches in one codebase is worse than either library, and it is the outcome nobody chooses deliberately.

People Also Ask

What is the main difference between Axios and fetch?

Error handling. Per MDN, a fetch promise "does not reject if the server responds with HTTP status codes that indicate errors" — you must check response.ok yourself. Axios rejects on non-2xx statuses. Everything else is convenience: Axios adds interceptors, automatic JSON, timeouts, progress and proxy configuration.

Is Axios still needed now that fetch is built in?

It depends on what you need. For simple requests, no. For interceptors, upload progress, per-instance defaults or Node proxy configuration, Axios still provides things fetch does not, and writing them yourself means maintaining a small library.

Why does fetch not throw on 404?

Because the transfer succeeded — the server was reached and it answered. fetch treats a 404 as a valid response rather than a failed request, and rejects only on network errors or a badly-formed URL. Check response.ok on every call.

Which is faster, Axios or fetch?

For a single request the difference is negligible; both are bound by the network. Axios adds a small amount of processing and, in the browser, a small amount of download for the library itself.

How do I set a timeout with fetch?

AbortSignal.timeout(5000) passed as the signal option in modern environments, or an AbortController with your own timer. There is no default timeout, so a request without one can hang indefinitely.

Does fetch work with proxies in Node?

Not via the usual environment variables. Node's global fetch is built on undici and does not read HTTP_PROXY or HTTPS_PROXY. You must supply a ProxyAgent dispatcher, either globally with setGlobalDispatcher or per request. Axios has a proxy config option instead.

Can I use a proxy with fetch in the browser?

No. Browser JavaScript cannot configure a proxy — the browser uses the system or extension settings. Any library's proxy option is a Node-only feature. Proxied requests from the browser must go through a server you control.

Should I use both Axios and fetch in one project?

It is not a problem in itself. What causes real trouble is several inconsistent homegrown wrappers with different error behaviour. Consistency in how failures are handled matters more than which library produced the request.

Wrapping Up

One difference is substantive and the rest is preference.

fetch does not reject on HTTP errors. MDN states it explicitly: a 404 or a 504 resolves, and you must check response.ok or response.status yourself. Miss that on one call and the failure surfaces somewhere else entirely, as a confusing error about data that was never valid. Axios rejects on non-2xx by default. Both positions are defensible; only one requires discipline on every single call, and discipline is not a property codebases retain under deadline.

Everything else comes down to what you are building. In the browser, fetch is built in and free, and for simple request patterns the boilerplate is trivial. On the server, bundle size stops mattering and the features fetch omits — timeouts, interceptors, retry, proxy configuration — are precisely what server code needs, so the balance tips the other way.

The test worth applying: if you have written a wrapper around fetch and it is now more than about thirty lines, you are maintaining a small HTTP library. That is a legitimate choice made deliberately and a poor one made by accident.

On proxies, and this is the part people lose an afternoon to: native fetch in Node ignores the standard proxy environment variables. Setting HTTPS_PROXY does nothing, the request goes direct, and nothing errors. Supply an undici ProxyAgent dispatcher, or use Axios's proxy option — and verify against an address-reporting endpoint rather than assuming.

We sell proxies and most server-side requests need none. Where you do need one, knowing which client you are using is the first debugging step, not the last.

Axios vs Fetch: Which Should You Use in 2026? | Geonode