Originally published on API Status Check.
API errors are inevitable. Whether you're integrating with Stripe, OpenAI, Twilio, or any other third-party API, you'll encounter HTTP status codes that signal something went wrong. But what does each code actually mean? And more importantly—is it your fault or theirs?
This comprehensive cheat sheet breaks down every common HTTP status code you'll encounter, with practical examples and handling strategies for each.
Quick Reference Table
| Code | Meaning | Who's Responsible | Retry? |
|---|---|---|---|
| 2xx - Success | |||
| 200 | OK | N/A | N/A |
| 201 | Created | N/A | N/A |
| 202 | Accepted | N/A | N/A |
| 204 | No Content | N/A | N/A |
| 3xx - Redirection | |||
| 301 | Moved Permanently | API Provider | Follow redirect |
| 302 | Found (Temporary) | API Provider | Follow redirect |
| 304 | Not Modified | N/A | No |
| 4xx - Client Errors | |||
| 400 | Bad Request | You | No (fix request) |
| 401 | Unauthorized | You | No (add auth) |
| 403 | Forbidden | You/Provider | No (check permissions) |
| 404 | Not Found | You/Provider | No (check endpoint) |
| 409 | Conflict | You | Maybe (resolve conflict) |
| 422 | Unprocessable Entity | You | No (fix data) |
| 429 | Too Many Requests | You | Yes (with backoff) |
| 5xx - Server Errors | |||
| 500 | Internal Server Error | Provider | Yes (exponential backoff) |
| 502 | Bad Gateway | Provider | Yes (exponential backoff) |
| 503 | Service Unavailable | Provider | Yes (exponential backoff) |
| 504 | Gateway Timeout | Provider | Yes (exponential backoff) |
Deep Dive: The Codes You'll Actually See
400 Bad Request: You Sent Something Wrong
What it means: Your request was malformed. Missing required fields, invalid JSON, wrong data types—something in your request doesn't match what the API expects.
Common causes:
- Invalid JSON syntax (missing comma, unclosed bracket)
- Missing required parameters
- Wrong data types (string instead of integer)
- Invalid enum values
How to handle it:
try {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
age: 'twenty-five' // Wrong! Should be a number
})
});
if (!response.ok) {
const error = await response.json();
console.error('Bad request:', error.message);
// Log the full error for debugging
console.error('Details:', error.details);
}
} catch (err) {
console.error('Request failed:', err);
}
Fix it: Read the error message carefully. Most APIs return detailed validation errors telling you exactly what's wrong. Don't retry—fix your request first.
401 Unauthorized: Missing or Invalid Authentication
What it means: You didn't provide credentials, or the ones you provided are invalid/expired.
Common causes:
- Missing API key or token
- Expired JWT token
- API key in wrong header (should be
Authorization: BearerorX-API-Key) - Revoked credentials
How to handle it:
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error('API_KEY not configured');
}
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
if (response.status === 401) {
// Token might be expired - refresh if possible
const newToken = await refreshToken();
// Retry with new token
return retryWithNewToken(newToken);
}
Fix it: Double-check your API key is correct, in the right header, and hasn't expired. For OAuth tokens, implement automatic refresh before they expire.
403 Forbidden: You Don't Have Permission
What it means: You're authenticated, but you're not allowed to access this resource. Think of it like having a valid ID but not being on the guest list.
Common causes:
- Trying to access another user's data
- Account doesn't have the right subscription tier
- API endpoint requires special permissions
- IP address not whitelisted
How to handle it:
if (response.status === 403) {
const error = await response.json();
// Check if it's a plan limitation
if (error.code === 'UPGRADE_REQUIRED') {
throw new Error('This feature requires a paid plan');
}
// Check if it's a permission issue
if (error.code === 'INSUFFICIENT_PERMISSIONS') {
throw new Error('User lacks required permissions');
}
// Otherwise, this is a genuine forbidden access
throw new Error('Access denied');
}
Fix it: Don't retry. Either upgrade your plan, request additional permissions, or avoid accessing this resource.
404 Not Found: The Resource Doesn't Exist
What it means: The endpoint or resource you're looking for doesn't exist.
Common causes:
- Typo in the URL (
/usresinstead of/users) - Resource was deleted
- Wrong API version in URL (
/v1/vs/v2/) - Resource ID is invalid
The tricky part: Is it you or them?
- Your fault: Typo, wrong ID, deleted resource
- Their fault: Endpoint was removed without notice, documentation is outdated
How to handle it:
async function getUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (response.status === 404) {
// Could be deleted user or invalid ID
console.warn(`User ${userId} not found`);
return null; // Handle gracefully
}
return response.json();
}
// For endpoints (not resources), check API status
async function safeApiCall(endpoint) {
try {
const response = await fetch(endpoint);
if (response.status === 404) {
// Might be API issue - check status page
console.error('Endpoint not found. Check API Status Check for outages.');
}
} catch (err) {
console.error('Request failed:', err);
}
}
Fix it: Verify your URL is correct. If it's a resource (user, order, etc.), handle gracefully—it might have been deleted. If it's an endpoint, check the API's changelog or status page.
409 Conflict: The Resource Already Exists
What it means: Your request conflicts with the current state of the resource. Most common when trying to create something that already exists.
Common causes:
- Duplicate user registration (email already exists)
- Trying to create a resource with an ID that's taken
- Concurrent modification conflicts
How to handle it:
async function createUser(email, password) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.status === 409) {
const error = await response.json();
if (error.code === 'EMAIL_EXISTS') {
throw new Error('An account with this email already exists');
}
}
return response.json();
}
Fix it: Don't retry with the same data. Either update the existing resource (PATCH) or change your data to avoid the conflict.
422 Unprocessable Entity: Valid JSON, Invalid Data
What it means: Your request is syntactically correct (valid JSON), but semantically wrong. The data doesn't make sense in the business logic context.
Common causes:
- Email format is invalid
- Date is in the past when it should be future
- Price is negative
- Password doesn't meet complexity requirements
How to handle it:
async function updateProduct(productId, data) {
const response = await fetch(`https://api.example.com/products/${productId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.status === 422) {
const error = await response.json();
// Usually contains field-level validation errors
console.error('Validation errors:', error.errors);
// { "errors": { "price": "must be positive", "sku": "already exists" } }
throw new ValidationError(error.errors);
}
return response.json();
}
Fix it: Read the validation errors and correct your data. Don't retry without changes.
429 Too Many Requests: Slow Down!
What it means: You've exceeded the API's rate limit. You're making too many requests too quickly.
Common causes:
- Tight loop making requests without delays
- Multiple concurrent requests without throttling
- Exceeded daily/hourly quota
How to handle it:
async function makeRequestWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status === 429) {
// Check for Retry-After header
const retryAfter = response.headers.get('Retry-After');
const waitTime = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, i) * 1000; // Exponential backoff
console.log(`Rate limited. Waiting ${waitTime}ms before retry ${i + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
Fix it: Implement exponential backoff, respect Retry-After headers, and consider implementing a request queue to stay under rate limits.
500 Internal Server Error: Something Broke on Their End
What it means: The API server encountered an unexpected error. This is a server-side problem, not your fault.
Common causes:
- Bug in the API code
- Database connection failure
- Unhandled exception
- Memory/resource exhaustion
How to handle it:
async function robustApiCall(url, options) {
let lastError;
const maxRetries = 3;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.status === 500) {
lastError = await response.json();
console.error(`Server error (attempt ${i + 1}/${maxRetries}):`, lastError);
// Exponential backoff: 1s, 2s, 4s
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
continue;
}
return response;
} catch (err) {
lastError = err;
}
}
// After retries fail, check if it's a widespread outage
console.error('API appears to be down. Check API Status Check.');
throw lastError;
}
Fix it: Retry with exponential backoff. If it persists, check the API's status page. Not your bug to fix.
502 Bad Gateway: The Proxy Can't Reach the Server
What it means: The API gateway or load balancer received an invalid response from an upstream server.
Common causes:
- API server is down
- API server is overloaded and timing out
- Network issues between gateway and API server
- Deployment in progress
How to handle it: Same as 500—retry with exponential backoff. This is often transient during deployments.
503 Service Unavailable: Temporarily Down
What it means: The API is temporarily unable to handle requests. Often returned during maintenance or when the service is overloaded.
Common causes:
- Scheduled maintenance
- Overloaded servers (traffic spike)
- Deliberate throttling under heavy load
- Deployment window
How to handle it:
if (response.status === 503) {
const retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
console.log(`Service unavailable. Retry after ${retryAfter} seconds`);
// Schedule retry
setTimeout(() => retryRequest(), parseInt(retryAfter) * 1000);
} else {
// No Retry-After header - use exponential backoff
console.log('Service temporarily unavailable. Retrying...');
}
}
Fix it: Respect the Retry-After header if present. Otherwise, retry with exponential backoff.
504 Gateway Timeout: The Server Took Too Long
What it means: The gateway/proxy gave up waiting for the upstream server to respond.
Common causes:
- API operation is taking too long (complex query, large file processing)
- Server is under heavy load
- Network latency issues
How to handle it: Retry with backoff, or if the operation is long-running, consider switching to an async pattern where the API returns immediately and you poll for results.
"Is It Me or the API?" Decision Tree
Got an error code?
│
├─ 4xx (400-499)? → Probably you
│ ├─ 400/422 → Fix your request data
│ ├─ 401 → Check your API key/auth
│ ├─ 403 → Check permissions/plan
│ ├─ 404 → Check URL/resource exists
│ └─ 429 → Slow down your requests
│
└─ 5xx (500-599)? → Probably them
├─ Retry a few times with backoff
├─ Still failing?
│ └─ Check API Status Check
│ ├─ Outage reported? → Not your fault, wait
│ └─ No outage? → Might be your request triggering a bug
└─ Success on retry? → Transient issue, you're good
Distinguishing Your Bugs from API Outages
Here's the key question: Is it just you, or is everyone affected?
Signs it's YOUR bug:
- Error is consistent and immediate
- Happens on every request to that endpoint
- Started after YOU made changes
- 4xx status codes (especially 400, 401, 403, 422)
- Other endpoints work fine
Signs it's an API OUTAGE:
- Started suddenly with no changes on your end
- 5xx status codes (500, 502, 503, 504)
- Intermittent—sometimes works, sometimes doesn't
- Multiple endpoints affected
- High response times even on successful requests
- Other users reporting issues on Twitter/forums
The Fast Way: Check API Status Check
Instead of guessing, check API Status Check to see real-time status for hundreds of APIs:
async function isApiDown(apiName) {
try {
const response = await fetch(
`https://apistatuscheck.com/api/status/${apiName}`
);
const data = await response.json();
return data.status === 'down' || data.status === 'degraded';
} catch {
return false; // Assume up if status check fails
}
}
// In your error handler
if (response.status >= 500) {
const apiDown = await isApiDown('openai');
if (apiDown) {
console.log('OpenAI is experiencing an outage. Not your fault.');
// Show user-friendly message, queue request, or use fallback
} else {
console.log('Unexpected 500 error. Investigate further.');
}
}
Best Practices for Error Handling
1. Always Log Full Error Responses
if (!response.ok) {
const errorBody = await response.text();
console.error({
status: response.status,
statusText: response.statusText,
body: errorBody,
headers: Object.fromEntries(response.headers),
url: response.url
});
}
2. Implement Retries with Exponential Backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
// Retry on 5xx or 429
if (response.status >= 500 || response.status === 429) {
const backoff = Math.min(1000 * Math.pow(2, i), 10000);
await new Promise(resolve => setTimeout(resolve, backoff));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
3. Respect Rate Limits Proactively
class RateLimiter {
constructor(maxRequests, timeWindow) {
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
this.requests = [];
}
async waitIfNeeded() {
const now = Date.now();
this.requests = this.requests.filter(t => now - t < this.timeWindow);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.timeWindow - (now - oldestRequest);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.requests.push(now);
}
}
const limiter = new RateLimiter(100, 60000); // 100 req/min
await limiter.waitIfNeeded();
await fetch('https://api.example.com/data');
4. Provide Context-Aware Error Messages
function getErrorMessage(status, context) {
const messages = {
400: `Invalid ${context.resource} data. Check your input.`,
401: `Authentication failed. Check your API key.`,
403: `You don't have permission to access this ${context.resource}.`,
404: `${context.resource} not found. It may have been deleted.`,
429: `Rate limit exceeded. Please wait before trying again.`,
500: `${context.apiName} is experiencing issues. Try again later.`,
502: `${context.apiName} is temporarily unavailable.`,
503: `${context.apiName} is under maintenance.`
};
return messages[status] || `Unexpected error (${status})`;
}
FAQ
Q: Should I retry 4xx errors?
No, except for 429 (rate limit). 4xx errors indicate a problem with your request—retrying the same request will get the same error.
Q: How long should I wait between retries?
Use exponential backoff: 1s, 2s, 4s, 8s. Respect Retry-After headers when present.
Q: How many times should I retry?
3-5 retries is typical. For critical operations, you might retry more, but implement a circuit breaker to fail fast if the API is down.
Q: What's the difference between 401 and 403?
- 401: "Who are you?" (authentication missing/invalid)
- 403: "I know who you are, but you can't do that" (insufficient permissions)
Q: Why do I get 500 errors intermittently?
Often caused by API bugs triggered by specific edge cases, or infrastructure issues like database connection pools being exhausted. If it's consistent with certain requests, report it. If random, it's likely infrastructure.
Q: Should I show raw HTTP codes to end users?
No. Translate them into user-friendly messages: "Invalid email address" instead of "400 Bad Request".
Take Action: Monitor APIs Before They Fail
Don't wait for errors to tell you an API is down. API Status Check monitors 200+ APIs in real-time and alerts you the moment an outage begins.
Stop guessing. Start monitoring. Sign up for free at apistatuscheck.com.
Top comments (0)