Error Responses

Handling HTTP error status codes gracefully

HTTP errors (4xx, 5xx status codes) don't automatically throw exceptions in fetch(). The Promise resolves successfully — you must check response.ok or response.status to detect errors.

This is one of the most common mistakes when using fetch(). Always check for errors before parsing the response body.

The Problem: Errors Don't Throw

// This does NOT throw for 404 or 500 errors! const response = await fetch('/api/users/999'); // response.ok is false for 4xx/5xx status codes console.log(response.ok); // false console.log(response.status); // 404 // The body might contain an error message from the server const data = await response.json(); console.log(data); // { error: "User not found" }

fetch() only rejects for network errors (no connection, DNS failure). HTTP error responses are still valid responses — the server responded, just with an error status.

The Solution: Check response.ok

async function fetchData(url) { const response = await fetch(url); if (!response.ok) { // Try to get error details from response body let errorMessage = `HTTP ${response.status}`; try { const errorData = await response.json(); errorMessage = errorData.message || errorData.error || errorMessage; } catch { // Response wasn't JSON, use status text errorMessage = response.statusText || errorMessage; } throw new Error(errorMessage); } return response.json(); } // Usage try { const user = await fetchData('/api/users/999'); } catch (error) { console.error('Request failed:', error.message); // Show user-friendly error message }

Custom Error Class

class HttpError extends Error { constructor(response, body = null) { super(`HTTP ${response.status}: ${response.statusText}`); this.name = 'HttpError'; this.status = response.status; this.statusText = response.statusText; this.body = body; } get isClientError() { return this.status >= 400 && this.status < 500; } get isServerError() { return this.status >= 500; } get isNotFound() { return this.status === 404; } get isUnauthorized() { return this.status === 401; } } async function fetchJSON(url, options) { const response = await fetch(url, options); if (!response.ok) { const body = await response.json().catch(() => null); throw new HttpError(response, body); } return response.json(); } // Usage with specific error handling try { const user = await fetchJSON('/api/users/999'); } catch (error) { if (error instanceof HttpError) { if (error.isNotFound) { showMessage('User not found'); } else if (error.isUnauthorized) { redirectToLogin(); } else if (error.isServerError) { showMessage('Server error. Please try again later.'); } } else { // Network error showMessage('Connection failed. Check your internet.'); } }

Try Different Status Codes

Click a button to see how different status codes are handled

Common Mistake

Don't assume fetch() throws on HTTP errors:

  • Network errors (offline, DNS failure) → Promise rejects
  • HTTP errors (404, 500) → Promise resolves, check response.ok