Fetch API Error Handling in Frontend Apps

Abstract generated cover for Fetch API Error Handling in Frontend Apps.

The Fetch API looks simple:

const response = await fetch("/api/posts");
const posts = await response.json();

That works for the happy path. Real frontend apps need more care. Requests can fail in several ways:

  • network failure
  • server error
  • validation error
  • unauthorized request
  • invalid JSON
  • timeout
  • cancelled request
  • unexpected response shape Good error handling is not about making the code look impressive. It is about making failures understandable and recoverable.

The Mental Model

A fetch request has multiple stages: ```plain text start request -> receive response -> check status -> parse body -> use data

Each stage can fail.
The server can return a response with an error status.
The network can fail before a response exists.
The response can contain invalid JSON.
The JSON can have a different shape than your app expects.
Treat those as different problems.
## Fetch Does Not Reject on 404
This surprises many developers.
\`fetch\` rejects for network-level failures.
It does not reject just because the server returned \`404\` or \`500\`.
You need to check \`response.ok\`:
```javascript
const response = await fetch("/api/posts");

if (!response.ok) {
  throw new Error(`Request failed: ${response.status}`);
}

const posts = await response.json();

`response.ok` is true for status codes in the 200 to 299 range. That check should be part of your normal request helper.

Handling JSON Carefully

Do not assume every response has valid JSON. This can fail:

const data = await response.json();

The server might return:

  • empty response
  • HTML error page
  • malformed JSON
  • different content type A safer helper can catch parsing errors separately. Conceptually:
async function readJson(response: Response) {
  try {
    return await response.json();
  } catch {
    throw new Error("Response was not valid JSON");
  }
}

For production code, you may want a richer error object with status, body text, and request context.

User-Facing Messages

Developer errors and user messages are different. Bad user message: ```plain text TypeError: Failed to fetch

Better user message:
```plain text
Could not load articles. Check your connection and try again.

The app can still log technical details for debugging. The user needs to know what happened and what they can do. For example:

  • try again
  • log in again
  • fix form fields
  • contact support
  • wait and retry later The message should match the failure.

Timeouts and Cancellation

Fetch does not have a simple timeout option by default. You can use `AbortController`:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);

try {
  const response = await fetch("/api/posts", {
    signal: controller.signal,
  });
} finally {
  clearTimeout(timeout);
}

Cancellation is also useful when a component unmounts or the user changes filters before the previous request finishes. You do not want old responses updating new UI state.

Retrying Requests

Retries can help with temporary failures. But not every request should be retried. Usually safe to retry:

  • loading read-only data
  • temporary network failures
  • some 502 or 503 responses Be careful retrying:
  • payment actions
  • order creation
  • destructive mutations
  • anything that may create duplicate side effects For important write operations, backend idempotency matters. The frontend should not blindly repeat risky requests.

Common Mistakes

Mistake 1: Only handling the happy path

The happy path is not enough. Every networked UI needs failure states.

Mistake 2: Treating all errors the same

A validation error is not the same as a server outage. Different failures should produce different UI behavior.

Mistake 3: Showing technical errors directly to users

Raw error messages are often confusing. Log technical details, but write user-facing messages for humans.

Where This Shows Up in Real Projects

Fetch error handling appears in almost every frontend app:

  • login forms
  • dashboards
  • content sites
  • admin panels
  • browser extensions
  • Electron apps
  • internal tools The more important the workflow, the more careful the error handling should be. If an article list fails to load, a retry button may be enough. If an order submission fails, the app needs to be much more precise.

Key Takeaways

  • Fetch does not reject just because the HTTP status is 404 or 500.
  • Check `response.ok`.
  • JSON parsing can fail separately from the request.
  • User messages should be clearer than developer errors.
  • Use cancellation for stale requests and timeouts.
  • Be careful retrying write operations.

    Related Articles

  • API Integrations for Business Software

  • TypeScript for Python Developers
  • Local Storage vs IndexedDB vs Cookies

← Back to Blog Index