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.
Both make HTTP requests, but they make different trade-offs around defaults, ergonomics, and footprint.
At a glance
| Feature | fetch (native) | axios (library) |
|---|---|---|
| Bundle cost | 0 (built in) | ~15KB min+gz |
| JSON parsing | Manual: await res.json() | Automatic |
| 4xx/5xx behavior | Resolves; you check res.ok | Rejects automatically |
| Request body serialization | Manual stringify | Auto-serialize JSON, supports FormData |
| Timeout | No built-in (use AbortController + setTimeout) | timeout: 5000 option |
| Cancellation | AbortController | AbortController (modern) or CancelToken |
| Interceptors | None | axios.interceptors.request/response |
| Progress events | Streams (download); upload via XHR fallback | onUploadProgress / onDownloadProgress (XHR) |
| Node compatibility | Available in Node 18+ | Has Node adapter; same API |
| XSRF token handling | Manual | Built-in (xsrfCookieName/xsrfHeaderName) |
| Transform pipeline | Manual | transformRequest / transformResponse |
Side-by-side
// 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();// 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
res.okcheck. 404, 500 →fetchresolves. Forgetting to check.okand calling.json()on an HTML error page gives you a confusing parse error.- JSON.stringify the body. Fetch sends
[object Object]if you forget. - Set Content-Type manually. Fetch doesn't infer it.
- No timeout. Without an AbortController + setTimeout, a hung request hangs forever.
credentials: 'include'for cross-origin cookies (axios haswithCredentials: true).
What axios gives you
Interceptors — global request/response middleware (auth injection, error normalization):
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):
axios.post('/upload', file, {
onUploadProgress: e => console.log(`${(e.loaded / e.total) * 100}%`),
});What fetch gives you that axios doesn't
- Streams —
res.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.
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.