Lập trình · 27/09/2026

JavaScript fetch: Why HTTP 404 Does Not Trigger catch

An API returns 404, but catch never runs. The UI tries to use the response and fails somewhere else. This happens when a fulfilled promise is treated as proof of HTTP success. With fetch, inspect each layer before consuming the data.

Hướng dẫn thiết lập CI cho Laravel bằng GitHub Actions

An API returns 404, but catch never runs. The UI tries to use the response and fails somewhere else. This happens when a fulfilled promise is treated as proof of HTTP success. With fetch, inspect each layer before consuming the data.

1. Receiving a response is not HTTP success

The MDN Fetch guide explains that an error status such as 404 does not automatically reject the fetch promise. Check response.ok, which represents a status in the 200–299 range.

const response = await fetch('/api/profile');
if (!response.ok) {
  throw new Error('HTTP ' + response.status);
}
const profile = await response.json();

The application explicitly throws the HTTP error here. An enclosing try/catch can then handle it. The API path is illustrative and is not guaranteed to exist on every website.

2. Diagnose three different failure layers

  • No usable response obtained: networking, browser policy, or cancellation may be involved. Do not label every such failure as a server-side 500.
  • An unacceptable HTTP status: a status is available for handling according to the API contract.
  • Body reading or validation fails: a successful status does not guarantee valid JSON or the expected structure.

For example, middleware redirecting to a login page may eventually return HTML instead of the expected JSON. Inspect the final URL, status, Content-Type, and body in the DevTools Network panel. A JSON error message alone may not reveal the cause.

3. A deliberately scoped JSON reader

This helper targets GET endpoints whose successful responses must contain JSON. It is not a universal client for downloads, HEAD requests, or empty-body APIs.

class HttpError extends Error {
  constructor(status) {
    super('HTTP ' + status);
    this.name = 'HttpError';
    this.status = status;
  }
}

async function readJson(url) {
  const response = await fetch(url, {
    headers: { Accept: 'application/json' }
  });

  if (!response.ok) {
    throw new HttpError(response.status);
  }

  return await response.json();
}

async function loadProfile() {
  try {
    const profile = await readJson('/api/profile');
    // Validate the profile schema before rendering.
    return profile;
  } catch (error) {
    if (error instanceof HttpError) {
      console.warn('Profile request failed:', error.status);
    } else {
      console.warn('Could not read profile data');
    }
    throw error;
  }
}

Accept expresses the desired format; it does not force the server to return JSON. The helper deliberately does not read HTTP error bodies. If an API provides structured error details, add a separate guarded reader rather than assuming every error page is JSON.

The catch block records the error category and rethrows. Callers of loadProfile must still handle rejection and update the UI. This example does not implement timeouts, cancellation, or schema validation.

4. Do not disguise every failure as empty data

Returning an empty array from catch may suggest the account has no data when the API is actually failing. Keep loading, successful, and failed states separate. An empty result belongs to the successful state only when valid data establishes that result.

Handle 204 according to the endpoint contract instead of calling json unconditionally. If an absent body is allowed, provide a dedicated branch. If a JSON profile is mandatory, an empty body violates that contract. Successful parsing also does not establish that names, IDs, or lists have the right types.

5. A practical testing checklist

  • 200 with valid JSON and the correct schema: show success.
  • 200 with HTML or malformed JSON: show a data-reading failure.
  • 200 with valid JSON but the wrong schema: reject it before rendering.
  • 404 or 500: enter the HTTP error branch without relying on automatic rejection.
  • 204: follow the endpoint contract rather than assuming an empty object.
  • Connection loss or cancellation: do not invent an HTTP status.

Avoid logging entire error bodies, tokens, or personal information. If retrying is necessary, design a policy for the operation instead of automatically repeating every request, particularly data-changing requests.

Conclusion: Separate receiving the response, checking its status, reading its body, and validating its shape. A catch block handles thrown failures; it is not an automatic HTTP validator.

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.