Back to Networking
Networking
easy
mid

What are the key differences between fetch and Axios?

fetch is the browser-native HTTP API: lean, promise-based, but bare-bones — you handle JSON parsing manually, errors don't reject on 4xx/5xx, no timeout built in, no XHR-based progress events. Axios is a third-party library wrapping XHR (Node: http) with auto JSON, interceptors, request/response transforms, automatic 4xx/5xx rejection, timeout, cancellation, and node+browser symmetry. Use fetch when you want zero dependencies and modern features (streams, AbortController); axios when you want batteries-included DX, interceptors, and consistent behavior.

8 min read·~5 min to think through

Both make HTTP requests, but they make different trade-offs around defaults, ergonomics, and footprint.

At a glance

Featurefetch (native)axios (library)
Bundle cost0 (built in)~15KB min+gz
JSON parsingManual: await res.json()Automatic
4xx/5xx behaviorResolves; you check res.okRejects automatically
Request body serializationManual stringifyAuto-serialize JSON, supports FormData
TimeoutNo built-in (use AbortController + setTimeout)timeout: 5000 option
CancellationAbortControllerAbortController (modern) or CancelToken
InterceptorsNoneaxios.interceptors.request/response
Progress eventsStreams (download); upload via XHR fallbackonUploadProgress / onDownloadProgress (XHR)
Node compatibilityAvailable in Node 18+Has Node adapter; same API
XSRF token handlingManualBuilt-in (xsrfCookieName/xsrfHeaderName)
Transform pipelineManualtransformRequest / transformResponse

Side-by-side

js
// fetch
const res = await fetch('/api/user', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice' }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
js
// axios
const { data } = await axios.post('/api/user', { name: 'Alice' });

Axios is shorter because it inferred Content-Type, serialized the body, rejected on non-2xx, and parsed JSON. Fetch is "explicit > implicit" — each of those is a separate line.

Things fetch makes you remember

  1. res.ok check. 404, 500 → fetch resolves. Forgetting to check .ok and calling .json() on an HTML error page gives you a confusing parse error.
  2. JSON.stringify the body. Fetch sends [object Object] if you forget.
  3. Set Content-Type manually. Fetch doesn't infer it.
  4. No timeout. Without an AbortController + setTimeout, a hung request hangs forever.
  5. credentials: 'include' for cross-origin cookies (axios has withCredentials: true).

What axios gives you

Interceptors — global request/response middleware (auth injection, error normalization):

js
axios.interceptors.request.use(config => {
  config.headers.Authorization = `Bearer ${getToken()}`;
  return config;
});

axios.interceptors.response.use(
  res => res,
  err => {
    if (err.response?.status === 401) refreshToken();
    return Promise.reject(err);
  }
);

Auto-rejecting non-2xx — try/catch is the entire error model; no need to remember res.ok.

Upload progress (XHR-based; fetch needs ReadableStream gymnastics):

js
axios.post('/upload', file, {
  onUploadProgress: e => console.log(`${(e.loaded / e.total) * 100}%`),
});

What fetch gives you that axios doesn't

  • Streamsres.body.getReader() for incremental parsing (great for LLM token streaming).
  • Service Worker integration — service workers intercept fetch, not XHR.
  • Modern Request/Response objects — composable with Cache API, push API.
  • Zero deps — relevant for bundle-size-sensitive apps.

When to pick which

  • Greenfield app with React Query / SWR / Tanstack Query: use fetch under the hood; the data-fetching library handles caching, retries, dedup — the historical reasons to reach for axios are absorbed there.
  • Lots of legacy code with global interceptors / auth pipelines: stick with axios.
  • Need upload progress, file uploads with progress UI: axios is easier (or use XHR directly).
  • Streaming responses (LLM tokens, SSE-like): fetch.
  • Service workers: fetch.
  • Bundle-size-sensitive (landing pages, embeds): fetch.
  • Node script / Node backend on Node 18+: fetch is fine; no need for axios anymore.

A small wrapper splits the difference

Most apps end up writing a thin fetch wrapper that adds: JSON convenience, 4xx/5xx rejection, baseURL, auth. That gives you axios-like ergonomics with no dependency.

js
export async function api(path, opts = {}) {
  const res = await fetch(`${BASE}${path}`, {
    headers: { 'Content-Type': 'application/json', ...opts.headers },
    body: opts.body ? JSON.stringify(opts.body) : undefined,
    ...opts,
  });
  if (!res.ok) throw Object.assign(new Error(res.statusText), { status: res.status });
  return res.headers.get('content-type')?.includes('json') ? res.json() : res.text();
}

Follow-up questions

  • How do you implement a fetch timeout?
  • What does axios do under the hood — XHR or fetch?
  • How do you cancel an in-flight request with each?
  • When would you prefer a data-fetching library (React Query/SWR) over either?

Common mistakes

  • Forgetting to check res.ok with fetch — calling .json() on an error response.
  • Calling res.json() twice on the same Response (body can only be read once).
  • Using axios just for the JSON-auto-parse and shipping 15KB you don't need.
  • Mixing axios and fetch in the same codebase — two error models, two auth layers.
  • Forgetting credentials: 'include' (fetch) or withCredentials: true (axios) for cookie-based auth.
  • Setting Content-Type: application/json on a FormData/multipart request (breaks the boundary).

Performance considerations

  • fetch has no library overhead. Axios adds ~15KB min+gz to the bundle; for landing pages and mobile, that's measurable. Both are bottlenecked by network, not the library. For high-throughput Node services, axios's connection pooling is slightly worse than native http.Agent; native fetch in Node 18+ uses undici which is generally faster than axios + http.

Edge cases

  • Fetch doesn't follow redirects to a different origin in 'no-cors' mode.
  • Axios CancelToken is deprecated; use AbortController in axios 0.22+.
  • Fetch in Node 18+ has subtle differences (DNS resolver, no proxy support without undici dispatcher).
  • Response cloning: const clone = res.clone() — needed if you want to read body twice (e.g., log + parse).
  • Axios's transformResponse runs even on errors — make sure transforms tolerate error payloads.

Real-world examples

  • React Query / SWR / TanStack Query default to fetch — they own the retries/caching layer.
  • Older Vue/React codebases standardized on axios for interceptor-heavy auth.
  • Node CLI tools moving from axios → native fetch as Node 18 LTS adoption grew.

Senior engineer discussion

Seniors should articulate that the choice is mostly about ergonomics + interceptors vs bundle + native streams, and that in 2025+ the right answer is usually 'fetch + a thin wrapper + React Query.' They should also know that the library choice is downstream of the data-fetching architecture — if you have React Query handling caching/retries/dedup, axios's value-add shrinks substantially.

Related questions