API Error Handling Patterns
Consistent error handling makes APIs predictable for consumers and debuggable for operators.
Standard Error Response
interface ApiError {
error: {
code: string; // Machine-readable: "VALIDATION_ERROR"
message: string; // Human-readable: "The email field is required"
details?: Array<{ // Field-level errors for forms
field: string;
message: string;
}>;
requestId?: string; // For support/debugging
};
}
HTTP Status Code Mapping
Client Errors (4xx)
| Status |
Code |
Use when |
| 400 |
BAD_REQUEST |
Malformed JSON, missing required fields |
| 401 |
UNAUTHORIZED |
No auth token or expired token |
| 403 |
FORBIDDEN |
Valid auth but insufficient permissions |
| 404 |
NOT_FOUND |
Resource does not exist |
| 409 |
CONFLICT |
Duplicate entry, version conflict |
| 422 |
UNPROCESSABLE_ENTITY |
Valid JSON but business rule violation |
| 429 |
RATE_LIMITED |
Too many requests |
Server Errors (5xx)
| Status |
Code |
Use when |
| 500 |
INTERNAL_ERROR |
Unexpected server failure |
| 502 |
BAD_GATEWAY |
Upstream service returned invalid response |
| 503 |
SERVICE_UNAVAILABLE |
Planned maintenance or overload |
| 504 |
GATEWAY_TIMEOUT |
Upstream service timed out |
Implementation Pattern
class AppError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: Array<{ field: string; message: string }>
) {
super(message);
}
}
// Usage
throw new AppError(404, "NOT_FOUND", "User not found");
throw new AppError(422, "VALIDATION_ERROR", "Invalid input", [
{ field: "email", message: "must be a valid email address" },
]);
Global Error Handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const requestId = req.headers["x-request-id"] || crypto.randomUUID();
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
details: err.details,
requestId,
},
});
}
// Unexpected errors — log full details, return generic message
console.error({ requestId, error: err.message, stack: err.stack });
return res.status(500).json({
error: {
code: "INTERNAL_ERROR",
message: "An unexpected error occurred",
requestId,
},
});
});
Retry Guidance for Clients
Include retry information in responses:
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests",
"retryAfter": 30
}
}
Retry rules:
- 4xx errors: Do not retry (except 429 with
retryAfter)
- 5xx errors: Retry with exponential backoff
- Network errors: Retry with backoff, max 3 attempts
- Idempotency: Only retry non-POST requests unless an idempotency key is used