API Bouncer

Buy me a coffee

How to Handle API Errors Like a Pro

Every API will fail at some point. Servers go down, rate limits get hit, networks drop, and responses come back malformed. The difference between a frustrating app and a great one is how it handles these failures. Here's a complete guide to API error handling.

Understanding HTTP status codes

When an API responds, it includes a three-digit status code that tells you what happened. Here are the ones you'll encounter most:

Success codes (2xx)

Client error codes (4xx) — something YOU did wrong

Server error codes (5xx) — something on THEIR end went wrong

Basic error handling pattern

Every API call in your app should follow this pattern:

async function callApi(url) { try { const response = await fetch(url); if (!response.ok) { let errorMessage = 'HTTP ' + response.status; try { const errorBody = await response.json(); errorMessage = errorBody.message || errorBody.error || errorMessage; } catch { errorMessage = response.statusText || errorMessage; } throw new Error(errorMessage); } return await response.json(); } catch (error) { if (error.name === 'TypeError') { throw new Error('Network error. Check your connection.'); } throw error; } }

Retry with exponential backoff

For transient errors (429, 500, 502, 503, 504), retrying often works. But don't retry immediately — use exponential backoff to give the server time to recover:

async function fetchWithRetry(url, options, maxRetries) { options = options || {}; maxRetries = maxRetries || 3; const retryableStatuses = [429, 500, 502, 503, 504]; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const response = await fetch(url, options); if (retryableStatuses.includes(response.status) && attempt < maxRetries) { const retryAfter = response.headers.get('Retry-After'); const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000 + Math.random() * 1000; await new Promise(r => setTimeout(r, delay)); continue; } return response; } catch (error) { if (attempt < maxRetries && error.name === 'TypeError') { await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000)); continue; } throw error; } } }

The random jitter (Math.random() * 1000) prevents the "thundering herd" problem where many clients retry at the same moment.

Show users helpful error messages

Your users don't need to see "HTTP 503 Service Unavailable." Translate technical errors into friendly messages:

function getUserMessage(status) { switch (status) { case 401: return 'Session expired. Please log in again.'; case 403: return 'You don\'t have access to this resource.'; case 404: return 'This content could not be found.'; case 429: return 'Too many requests. Please wait a moment.'; default: if (status >= 500) return 'Something went wrong on our end. Please try again.'; return 'An unexpected error occurred.'; } }

Common mistakes to avoid

Testing error handling

Don't wait for errors to happen in production. Test them intentionally:

Good error handling isn't glamorous, but it's what separates apps that feel polished from apps that feel broken.