How to Handle API Errors: Status Codes and Error Objects
How to Handle API Errors: Status Codes and Error Objects
Bad error handling is the number one developer experience complaint about APIs. Generic "Something went wrong" messages waste debugging time. Missing status codes break client error handling. Inconsistent error formats require per-endpoint error parsing. Here's how to handle errors properly on both sides.
HTTP Status Codes: The Complete Guide
2xx — Success
| Code | Name | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, DELETE with response body |
| 201 | Created | Successful POST that creates a resource (include Location header) |
| 202 | Accepted | Request accepted for async processing (not yet completed) |
| 204 | No Content | Successful DELETE or PUT with no response body |
4xx — Client Errors
| Code | Name | When to Use |
|---|---|---|
| 400 | Bad Request | Malformed request (invalid JSON, missing required fields) |
| 401 | Unauthorized | Missing or invalid authentication credentials |
| 403 | Forbidden | Authenticated but not authorized for this resource/action |
| 404 | Not Found | Resource doesn't exist at this URL |
| 405 | Method Not Allowed | HTTP method not supported for this endpoint |
| 409 | Conflict | Resource state conflict (duplicate, version mismatch) |
| 410 | Gone | Resource existed but has been permanently deleted |
| 422 | Unprocessable Entity | Request is well-formed but semantically invalid |
| 429 | Too Many Requests | Rate limit exceeded (include Retry-After header) |
5xx — Server Errors
| Code | Name | When to Use |
|---|---|---|
| 500 | Internal Server Error | Unexpected server failure (bug, unhandled exception) |
| 502 | Bad Gateway | Upstream service returned an invalid response |
| 503 | Service Unavailable | Server is temporarily overloaded or in maintenance |
| 504 | Gateway Timeout | Upstream service didn't respond in time |
The Minimum Set
If you only use a few, use these: 200, 201, 204, 400, 401, 403, 404, 409, 422, 429, 500, 503.
Error Response Format
The Standard Error Object
{
"error": {
"type": "validation_error",
"code": "invalid_parameter",
"message": "The 'email' field must be a valid email address.",
"param": "email",
"request_id": "req_abc123def456",
"doc_url": "https://api.example.com/docs/errors#invalid_parameter"
}
}
With Multiple Validation Errors
{
"error": {
"type": "validation_error",
"message": "Request validation failed.",
"errors": [
{
"field": "email",
"code": "invalid_format",
"message": "Must be a valid email address."
},
{
"field": "password",
"code": "too_short",
"message": "Must be at least 8 characters.",
"metadata": { "min_length": 8, "actual_length": 5 }
}
],
"request_id": "req_abc123"
}
}
Essential Fields
| Field | Purpose | Required |
|---|---|---|
type | Error category (validation_error, authentication_error, rate_limit_error) | Yes |
code | Machine-readable error code (invalid_parameter, not_found, rate_limited) | Yes |
message | Human-readable explanation | Yes |
request_id | Unique ID for debugging and support | Yes |
param / field | Which parameter/field caused the error | For validation errors |
doc_url | Link to documentation about this error | Recommended |
metadata | Additional context (limits, allowed values, etc.) | Optional |
How Top APIs Handle Errors
Stripe
{
"error": {
"type": "card_error",
"code": "card_declined",
"decline_code": "insufficient_funds",
"message": "Your card has insufficient funds.",
"param": "source",
"charge": "ch_abc123"
}
}
Stripe's errors include the relevant object ID (charge), decline codes, and the specific parameter that caused the error.
GitHub
{
"message": "Validation Failed",
"errors": [
{
"resource": "Issue",
"field": "title",
"code": "missing_field"
}
],
"documentation_url": "https://docs.github.com/rest"
}
GitHub includes the resource type and a documentation URL.
Twilio
{
"code": 20003,
"message": "Permission denied",
"more_info": "https://www.twilio.com/docs/errors/20003",
"status": 403
}
Twilio uses numeric error codes with a direct link to detailed error documentation.
Client-Side Error Handling
The Error Handling Hierarchy
- Network errors — no response received (timeout, DNS failure, connection refused)
- HTTP errors — response received with error status code
- Application errors — 200 response but with error in the body (some APIs do this)
Pattern: Retry Strategy
| Error Type | Should Retry? | Strategy |
|---|---|---|
| 400 (Bad Request) | No | Fix the request |
| 401 (Unauthorized) | No | Re-authenticate |
| 403 (Forbidden) | No | Check permissions |
| 404 (Not Found) | No | Resource doesn't exist |
| 409 (Conflict) | Maybe | Re-read and retry |
| 429 (Rate Limited) | Yes | Wait Retry-After seconds |
| 500 (Server Error) | Yes | Exponential backoff |
| 502 (Bad Gateway) | Yes | Exponential backoff |
| 503 (Unavailable) | Yes | Wait Retry-After or backoff |
| Network timeout | Yes | Exponential backoff |
Pattern: Error Classification
Classify errors into three categories for your application:
- Retryable — 429, 500, 502, 503, 504, network timeouts → retry with backoff
- Fixable — 400, 401, 422 → user can fix the input
- Terminal — 403, 404, 409 → can't be fixed by retrying or changing input
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| 200 for everything | Clients can't distinguish success/failure by status code | Use proper HTTP status codes |
| Plain text error messages | No machine parsing | JSON error objects |
| No request ID | Debugging requires reproducing the issue | Include request_id in every error |
| Generic "Server Error" | No debugging information | Specific error codes and messages |
| Stack traces in production | Security vulnerability (leaks internals) | Log server-side, return generic 500 |
| Missing Retry-After on 429 | Clients retry immediately, making it worse | Always include Retry-After |
| Different error formats per endpoint | Client needs per-endpoint error handling | Consistent error schema |
Designing API error handling? Explore API best practices and tools on APIScout — architecture guides, comparisons, and developer resources.