Every APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. returns errors. The question is whether those errors help the caller fix the problem or leave them guessing. Most developers know that 404 means "not found" and 500 means "something broke," but in integration work, the nuances of HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. status codes matter far more than you might expect. The wrong status codeWhat is status code?A three-digit number in an HTTP response that tells the client what happened: 200 means success, 404 means not found, 500 means the server broke. can cause a caller to retry forever, crash a retry loop, or silently drop important data.
if (response.status >= 400) as a catch-all. This retries 400 Bad Request, 403 Forbidden, and 404 Not Found, all errors that will never succeed on retry. Always verify that AI is only retrying the codes that make sense: 429 and 5xx.The caller's perspective
When you are building an integration, you are the caller. You receive HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. status codes and must decide what to do with them. This is fundamentally different from building a server, where you choose which codes to send. As a caller, you need a clear strategy for each category.
2xx -- success
The request worked. Move on. But even within success codes, the distinctions matter.
// Different 2xx codes mean different things
// 200 OK -- here is the data you asked for
// 201 Created -- the resource was created, check the Location header
// 202 Accepted -- we received your request but haven't processed it yet
// 204 No Content -- success, but there's nothing to return
const response = await fetch('https://api.example.com/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderData),
});
if (response.status === 201) {
const location = response.headers.get('Location');
console.log(`Order created at: ${location}`);
} else if (response.status === 202) {
// The order is queued -- poll or wait for a webhook
console.log('Order accepted for processing');
}A 202 Accepted is particularly important in integrations. It means the server took your request but has not finished processing it. If you treat 202 the same as 200, you might assume the work is done when it has barely started.
4xx -- client errors
The request is wrong. Something about what you sent is invalid, unauthorized, or points to a resource that does not exist. The key insight: retrying the exact same request will produce the exact same error. You must change something before trying again.
async function handleClientError(response) {
switch (response.status) {
case 400: // Bad Request -- your payload is malformed
const error = await response.json();
console.error('Validation failed:', error);
// Fix the payload, do NOT retry as-is
break;
case 401: // Unauthorized -- token expired or missing
// Refresh the token, then retry
await refreshAuthToken();
break;
case 403: // Forbidden -- valid token but insufficient permissions
// Do NOT retry -- you need different permissions
console.error('Insufficient permissions');
break;
case 404: // Not Found -- resource doesn't exist
// Maybe it was deleted, or the ID is wrong
break;
case 409: // Conflict -- resource state conflict (e.g., duplicate)
// Often means the operation already happened -- check idempotency
break;
case 422: // Unprocessable Entity -- valid JSON but semantically wrong
// The server understood the format but rejected the content
break;
case 429: // Too Many Requests -- you're being rate-limited
const retryAfter = response.headers.get('Retry-After');
// Wait the specified time, then retry
break;
}
}Notice that 429 is the exception in the 4xx family. It is a client error (you sent too many requests), but retrying after a delay is exactly the right response.
5xx -- server errors
The server failed. This might be temporary (a deployment in progress, a spike in traffic) or persistent (a bug, a crashed dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself.). The caller's job is to retry with backoff and eventually give up.
async function handleServerError(response) {
switch (response.status) {
case 500: // Internal Server Error -- generic server failure
// Retry with backoff -- might be a transient bug
break;
case 502: // Bad Gateway -- upstream service is down
// The server's dependency failed, retry later
break;
case 503: // Service Unavailable -- server is overloaded or in maintenance
const retryAfter = response.headers.get('Retry-After');
// Respect Retry-After header if present
break;
case 504: // Gateway Timeout -- upstream service was too slow
// Similar to 502, retry with backoff
break;
}
}The HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. status codeWhat is status code?A three-digit number in an HTTP response that tells the client what happened: 200 means success, 404 means not found, 500 means the server broke. reference
Here is the practical reference you actually need. Not every code that exists, but every code you will encounter in real integrations.
| Code | Name | Retry-safe? | Real-world example |
|---|---|---|---|
| 200 | OK | N/A | Fetching a user profile |
| 201 | Created | N/A | Creating a new order |
| 202 | Accepted | N/A | Submitting a batch job |
| 204 | No Content | N/A | Deleting a resource |
| 400 | Bad Request | No | Sending invalid JSON |
| 401 | Unauthorized | After re-auth | Expired JWT token |
| 403 | Forbidden | No | Accessing admin-only route |
| 404 | Not Found | No | Requesting a deleted resource |
| 409 | Conflict | No | Creating a duplicate username |
| 422 | Unprocessable Entity | No | Valid JSON but invalid email format |
| 429 | Too Many Requests | Yes, after delay | Hitting Stripe's rate limit |
| 500 | Internal Server Error | Yes, with backoff | Unhandled exception in server code |
| 502 | Bad Gateway | Yes, with backoff | Nginx can't reach the app server |
| 503 | Service Unavailable | Yes, with backoff | Server is deploying or overloaded |
| 504 | Gateway Timeout | Yes, with backoff | Database query took too long |
RFC 7807 problem details
Most APIs return errors as unstructured JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it.. Each APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. invents its own format, which means every caller has to write custom parsing logic.
RFC 7807 (now updated as RFC 9457) defines a standard format called Problem Details. It gives errors a consistent structure that machines can parse and humans can read.
Bad error response (custom, inconsistent)
{
"error": true,
"message": "Something went wrong",
"code": 42
}What is code 42? What went wrong? What should the caller do? This response is useless to an automated system and barely helpful to a human.
Good error response (RFC 7807)
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient Funds",
"status": 422,
"detail": "Account 12345 has a balance of CODE_BLOCK0.00, but the transfer requires $50.00.",
"instance": "/transfers/txn-789",
"balance": 10.00,
"required": 50.00
}| Field | Required? | Purpose |
|---|---|---|
type | Yes | A URI identifying the error type (can be a docs URL) |
title | Yes | A short, human-readable summary |
status | Yes | The HTTP status code (repeated for convenience) |
detail | No | A human-readable explanation specific to this occurrence |
instance | No | A URI identifying this specific error occurrence |
| (extensions) | No | Any additional fields relevant to this error type |
Implementing problem details in your API
interface ProblemDetails {
type: string;
title: string;
status: number;
detail?: string;
instance?: string;
[key: string]: unknown; // extension fields
}
function problemResponse(res: Response, problem: ProblemDetails) {
return new Response(JSON.stringify(problem), {
status: problem.status,
headers: {
'Content-Type': 'application/problem+json', // RFC 7807 media type
},
});
}
// Usage in a route handler
if (!account || account.balance < amount) {
return problemResponse(res, {
type: 'https://api.example.com/errors/insufficient-funds',
title: 'Insufficient Funds',
status: 422,
detail: `Account ${accountId} has $${account.balance}, needs $${amount}.`,
instance: `/transfers/${transferId}`,
balance: account.balance,
required: amount,
});
}The Content-Type: application/problem+json header tells the caller that this is a structured error response, not just regular JSON. Clients can detect this and parse errors consistently across any API that follows the standard.
Designing for debuggability
When an integration fails at 3 AM, the error response is the first thing anyone looks at. A few principles make errors dramatically easier to debug.
Include a request ID. Every request should get a unique identifier that appears in both the response and your server logs. When a caller reports an error, you can trace it instantly.
// Middleware that adds a request ID
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
res.setHeader('X-Request-Id', requestId);
req.requestId = requestId;
next();
});
// Include it in error responses
return problemResponse(res, {
type: 'https://api.example.com/errors/not-found',
title: 'Resource Not Found',
status: 404,
detail: `Order ${orderId} does not exist.`,
instance: `/orders/${orderId}`,
requestId: req.requestId, // Traceable
});Be specific, not generic. "Internal Server Error" tells the caller nothing. "Database connection poolWhat is connection pool?A set of pre-opened database connections that your app reuses instead of opening and closing a new one for every request. exhausted" tells them to back off and retry later. You do not need to expose implementation details, but you do need to communicate actionable information.
Include timestamps. When the error happened matters for debugging timeouts and race conditions.
These practices turn error responses from obstacles into tools. When your integration partner calls to say something is broken, you want the error response to tell both of you exactly what happened and what to do next.