When you compare fetch vs axios in TypeScript, the core difference in error handling is that axios automatically rejects its promise for any non-2xx HTTP status (like 404 or 500), while the native fetch API only rejects on network failures, leaving you to manually check response.ok and throw your own error. This single design choice drives most of the practical tradeoffs: axios gives you a consistent error object with the response attached, while fetch forces you to build that structure yourself but keeps your bundle smaller and your code dependency-free.
Background: Why the Comparison Exists
TypeScript developers have used fetch (the browser-native API, now also in Node.js 18+) and axios (a third-party promise-based HTTP client) side by side for years. Fetch became the modern default because it is built into the platform, but its error model is famously minimal. Axios, released in 2014, was designed to smooth over the rough edges of XMLHttpRequest and later adopted by many who found fetch too low-level. In TypeScript specifically, both libraries have type definitions, but they differ sharply in how much type safety they give you around errors and responses.
The practical context: you are likely choosing between them for a new project or migrating an existing one. The decision affects not just how you catch errors, but how you model API responses, how you handle timeouts, and how much boilerplate you write. This article focuses on the error-handling features and their tradeoffs, with concrete TypeScript examples.
How Fetch Handles Errors in TypeScript
Fetch’s promise only rejects when the request itself fails to complete—DNS failure, network drop, CORS block, or an aborted request. An HTTP 404 or 500 is still a successful fetch; the promise resolves with a Response object whose ok property is false.
Here is the standard typed pattern you must write yourself:
async function getData(url: string): Promise<MyData> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<MyData>;
}
Notice what is missing: the response body of the error is not captured. If your server returns a JSON error payload like { "message": "Invalid token" }, you have to read it manually and decide how to attach it to the thrown error. A common pattern is to create a custom error class:
class HttpError extends Error {
constructor(public status: number, public body: unknown) {
super(`HTTP ${status}`);
}
}
const response = await fetch(url);
if (!response.ok) {
const body = await response.json().catch(() => null);
throw new HttpError(response.status, body);
}
This works, but you must replicate it for every request. The tradeoff: fetch gives you total control, but zero built-in structure for error handling. In TypeScript, you also have to cast the JSON response to your type, because response.json() returns Promise<any>.
How Axios Handles Errors in TypeScript
Axios rejects the promise for any HTTP status outside the 2xx range. The rejection is an AxiosError object with a response property containing the status, headers, and data. This is the key feature: you get the error body automatically, no extra code.
Typical usage:
import axios, { AxiosError } from 'axios';
try {
const response = await axios.get<MyData>('/api/data');
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const message = error.response?.data?.message ?? error.message;
// handle 4xx/5xx with server-provided message
} else {
// network error or unexpected
}
}
The axios.isAxiosError type guard narrows the error type in TypeScript, giving you typed access to response, request, and config. Axios also distinguishes between a request that was made but got no response (network error) and a request that never left the client (config error). The error.request property exists in the first case, and error.response in the second.
The tradeoff: you get a richer, more consistent error object, but you are tied to axios’s error model. If you ever switch libraries, you must rewrite your error handling. Also, axios’s automatic rejection means you cannot accidentally forget to check ok—but it also means you must wrap every call in try/catch, even when you expect a 404 as a valid outcome (for example, checking if a resource exists).
Type Safety: The TypeScript-Specific Tradeoff
TypeScript’s power comes from knowing types at compile time. Here, fetch and axios differ significantly.
Fetch:
response.json()returnsPromise<any>, so you must cast to your type. This is a type assertion, not a guarantee—if the server sends malformed JSON, you get a runtime error, not a compile-time one.- Error handling is manual, so you define your own error types. This is flexible but easy to get inconsistent across a codebase.
- No built-in way to type the error body; you must create a custom class or interface.
Axios:
axios.get<MyData>(url)types the response data directly.response.dataisMyData.AxiosErrorhas a generic parameter for the error response body:AxiosError<{ message: string }>. This lets you type the server’s error payload.- Interceptors can be typed to transform requests/responses globally, which can also centralize error normalization.
The practical effect: axios gives you more type safety out of the box, especially for error bodies. Fetch requires you to build a type-safe wrapper yourself. If you value compile-time checks on error data, axios is easier. If you prefer minimal dependencies and are comfortable writing a small helper, fetch can be made type-safe too.
Common Problems and Misconceptions
Misconception: fetch rejects on 404. It does not. This is the most common surprise for developers moving from axios to fetch. You must check response.ok or response.status manually.
Misconception: axios errors are always HTTP errors. Axios also rejects on network errors, timeouts, and aborts. The error.response property is only present for HTTP errors; for network errors, error.request is set, and for aborts, error.code === 'ECONNABORTED' (for timeouts) or ERR_CANCELED (for cancellation).
Problem: fetch and HTTP error bodies. If your server returns a JSON error body, fetch does not parse it automatically. You must call response.json() inside the error branch, which itself can fail if the body is not JSON. This leads to nested try/catch or .catch() chains.
Problem: TypeScript’s unknown in catch blocks. In strict mode, the catch variable is unknown, not any. With fetch, you have to narrow it yourself. With axios, axios.isAxiosError is a type guard that narrows to AxiosError.
Problem: timeout handling. Fetch does not have a built-in timeout. You must use AbortController with setTimeout. Axios has a timeout config option that automatically aborts and rejects with ECONNABORTED. This is a significant convenience for error handling.
How to Choose: Practical Decision Factors
There is no universally correct answer; it depends on your project’s constraints. Consider these factors:
- Bundle size: Fetch is zero bytes. Axios adds roughly 5-8 kB gzipped. If you are building a tiny library or a performance-critical page, fetch wins.
- Error body consistency: If your API always returns structured JSON errors, axios saves you from writing the same parsing logic everywhere.
- Timeout and cancellation: Axios has built-in timeout and cancellation (via
AbortControllerin newer versions). Fetch requires manual AbortController wiring. - TypeScript strictness: Axios’s generic error type is more convenient. But you can achieve similar with a fetch wrapper.
- Legacy browser support: Axios works in older browsers. Fetch needs a polyfill.
- Team familiarity: If your team already knows axios patterns, switching to fetch may introduce subtle bugs from the different error model.
A common middle ground is to write a thin wrapper around fetch that mimics axios’s error behavior. This gives you the bundle size benefit while standardizing error handling. But that wrapper is code you must maintain and test.
Costs and Maintenance Tradeoffs
Axios’s convenience comes with a long-term cost: it is a dependency you must keep updated, and its API surface is larger, which can lead to more breaking changes over time. Fetch is part of the platform, so it evolves with the browser/Node.js, but its API is stable and minimal—you will never get a built-in timeout or error body parsing.
Maintenance also differs. With fetch, your error-handling code lives in your project, so you control it fully. With axios, you rely on the library’s decisions (e.g., what counts as an error). For most projects, the difference is minor, but for a large codebase with many API calls, the consistency axios provides can reduce bugs.
Another cost: axios’s automatic rejection means that a 401 or 403 will throw, which is often what you want, but if you need to handle those statuses as non-errors (e.g., a login endpoint returning 401 for wrong password), you must catch them specifically. Fetch forces you to be explicit, which some argue is better because it makes the control flow visible.
Alternatives and Future Directions
Beyond fetch and axios, there are other options that address the error-handling gap. Ky is a small fetch wrapper that adds timeout, retry, and a simpler error model. React Query (TanStack Query) is not an HTTP client but wraps either fetch or axios and provides error state management, which can reduce the need for manual error handling in UI code. tRPC or GraphQL clients like Apollo handle errors at a different layer, often making HTTP error details irrelevant.
In the future, fetch may gain more features, but as of now, there is no sign of built-in timeout or automatic error body parsing. Axios continues to be maintained, but its API is stable. The choice remains a tradeoff between convenience and control.
Frequently Asked Questions
Does fetch reject on 404? No. Fetch only rejects on network-level failures. A 404 or 500 resolves the promise; you must check response.ok or response.status and throw your own error if you want to treat it as an exception.
Can I make fetch behave like axios for errors? Yes. Write a wrapper function that checks response.ok, parses the error body, and throws a custom error with status and data. This gives you axios-like behavior without the dependency.
Is axios’s error object better for TypeScript? Generally yes, because AxiosError<T> lets you type the error response body, and axios.isAxiosError is a built-in type guard. With fetch, you must define your own error class and narrow the unknown catch variable manually.
Which is faster: fetch or axios? In terms of raw request speed, they are comparable. Fetch may be slightly faster due to less overhead, but the difference is negligible for most applications. The bigger performance factor is the size of the JavaScript bundle, where fetch has the advantage.
Should I use fetch in Node.js? Node.js 18+ includes fetch natively, so you can use it server-side. However, Node’s fetch does not have a built-in timeout, and its error handling is the same as the browser. Axios works identically in both environments, which can simplify code if you share logic between client and server.
Can I use interceptors with fetch? Not natively. Axios has request and response interceptors that can centralize error handling (e.g., logging, token refresh). With fetch, you must implement your own wrapper or use a library like Ky that offers similar hooks.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- Valheim Server Not Showing in Community List Fixes
- DisplayPort vs HDMI: Which Cable Should You Use?
- Soundbar vs 2.1 Speaker System: Which Is Better?
- Survive Lava for Brainrot Tier List170 points
- MySQL Dump Import: Fix Common Errors
- Mouse Moving on Its Own? Causes & Fixes