12 KiB
API Error Handling
Error Response Design
Consistent, informative error responses are critical for API usability.
Standard Error Format
Basic Error Response
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User with ID 123 not found",
"details": null
}
}
RFC 7807 Problem Details
Standardized error format (application/problem+json):
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/resource-not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "User with ID 123 does not exist",
"instance": "/users/123"
}
Fields:
type- URI reference identifying error typetitle- Short, human-readable summarystatus- HTTP status codedetail- Human-readable explanation specific to this occurrenceinstance- URI reference for this specific occurrence
Extended Error Response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email must be a valid email address"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Age must be between 18 and 120"
}
],
"request_id": "req_123456",
"timestamp": "2024-01-15T10:30:00Z",
"documentation_url": "https://api.example.com/docs/errors#validation-error"
}
}
Error Categories
1. Validation Errors (400 Bad Request)
Client sent invalid data.
POST /users
Content-Type: application/json
{
"name": "",
"email": "invalid-email",
"age": 15
}
Response: 400 Bad Request
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "name",
"code": "REQUIRED",
"message": "Name is required"
},
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email must be a valid email address"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Age must be at least 18",
"constraints": {
"min": 18,
"max": 120
}
}
]
}
}
2. Authentication Errors (401 Unauthorized)
Missing or invalid authentication credentials.
GET /users/123
Authorization: Bearer invalid_token
Response: 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token"
{
"error": {
"code": "INVALID_TOKEN",
"message": "The access token is invalid or has expired",
"details": {
"reason": "token_expired",
"expired_at": "2024-01-15T10:00:00Z"
}
}
}
Common auth error codes:
MISSING_TOKEN- No auth token providedINVALID_TOKEN- Token is malformed or invalidEXPIRED_TOKEN- Token has expiredREVOKED_TOKEN- Token has been revoked
3. Authorization Errors (403 Forbidden)
Authenticated but not authorized to perform action.
DELETE /users/123
Authorization: Bearer valid_token
Response: 403 Forbidden
{
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "You do not have permission to delete this user",
"details": {
"required_permission": "users:delete",
"your_permissions": ["users:read", "users:update"]
}
}
}
4. Not Found Errors (404 Not Found)
Resource doesn't exist.
GET /users/99999
Response: 404 Not Found
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User with ID 99999 not found",
"details": {
"resource_type": "User",
"resource_id": "99999"
}
}
}
5. Conflict Errors (409 Conflict)
Request conflicts with current state.
POST /users
Content-Type: application/json
{
"email": "existing@example.com",
"name": "John Doe"
}
Response: 409 Conflict
{
"error": {
"code": "RESOURCE_ALREADY_EXISTS",
"message": "User with email 'existing@example.com' already exists",
"details": {
"field": "email",
"value": "existing@example.com",
"existing_resource": "/users/123"
}
}
}
6. Rate Limiting (429 Too Many Requests)
Client exceeded rate limit.
GET /users
Response: 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705320000
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit",
"details": {
"limit": 100,
"window": "1 hour",
"retry_after": 60,
"reset_at": "2024-01-15T11:00:00Z"
}
}
}
7. Server Errors (500 Internal Server Error)
Unexpected server error.
GET /users/123
Response: 500 Internal Server Error
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred. Please try again later.",
"request_id": "req_123456",
"timestamp": "2024-01-15T10:30:00Z"
}
}
Never expose:
- Stack traces
- Database errors
- Internal paths
- Sensitive configuration
8. Service Unavailable (503 Service Unavailable)
Service temporarily unavailable.
GET /users
Response: 503 Service Unavailable
Retry-After: 300
{
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Service is temporarily unavailable due to maintenance",
"details": {
"retry_after": 300,
"maintenance_end": "2024-01-15T12:00:00Z"
}
}
}
Error Code Catalog
Define standard error codes for your API:
{
"VALIDATION_ERROR": {
"status": 400,
"description": "Request validation failed",
"subcodes": {
"REQUIRED": "Required field is missing",
"INVALID_FORMAT": "Field has invalid format",
"OUT_OF_RANGE": "Value is out of allowed range",
"INVALID_ENUM": "Value is not in allowed set"
}
},
"AUTHENTICATION_ERROR": {
"status": 401,
"description": "Authentication failed",
"subcodes": {
"MISSING_TOKEN": "No authentication token provided",
"INVALID_TOKEN": "Token is invalid",
"EXPIRED_TOKEN": "Token has expired"
}
},
"AUTHORIZATION_ERROR": {
"status": 403,
"description": "Insufficient permissions",
"subcodes": {
"INSUFFICIENT_PERMISSIONS": "Missing required permission",
"RESOURCE_FORBIDDEN": "Access to resource is forbidden"
}
},
"RESOURCE_NOT_FOUND": {
"status": 404,
"description": "Resource not found"
},
"CONFLICT_ERROR": {
"status": 409,
"description": "Request conflicts with current state",
"subcodes": {
"RESOURCE_ALREADY_EXISTS": "Resource already exists",
"CONCURRENT_MODIFICATION": "Resource was modified by another request"
}
},
"RATE_LIMIT_EXCEEDED": {
"status": 429,
"description": "Rate limit exceeded"
},
"INTERNAL_SERVER_ERROR": {
"status": 500,
"description": "Internal server error"
}
}
Validation Error Details
Field-Level Validation
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "credit_card.number",
"code": "INVALID_FORMAT",
"message": "Credit card number must be 16 digits",
"value_provided": "1234",
"constraints": {
"pattern": "^[0-9]{16}$"
}
},
{
"field": "items[0].quantity",
"code": "OUT_OF_RANGE",
"message": "Quantity must be at least 1",
"value_provided": 0,
"constraints": {
"min": 1,
"max": 1000
}
}
]
}
}
Cross-Field Validation
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"fields": ["start_date", "end_date"],
"code": "INVALID_RANGE",
"message": "End date must be after start date",
"values_provided": {
"start_date": "2024-01-20",
"end_date": "2024-01-15"
}
}
]
}
}
Request ID Tracking
Always include request ID for debugging:
Response Headers:
X-Request-ID: req_abc123
Response Body:
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred",
"request_id": "req_abc123"
}
}
Clients can reference request ID in support tickets.
Error Documentation
Document all possible errors for each endpoint:
/users/{id}:
get:
responses:
'200':
description: Success
'401':
description: Authentication failed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
missing_token:
value:
error:
code: MISSING_TOKEN
message: No authentication token provided
invalid_token:
value:
error:
code: INVALID_TOKEN
message: Token is invalid or expired
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
not_found:
value:
error:
code: RESOURCE_NOT_FOUND
message: User with ID 123 not found
Retry Guidance
Help clients understand if they should retry:
{
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Service temporarily unavailable",
"retry": {
"retryable": true,
"retry_after": 60,
"max_retries": 3,
"backoff": "exponential"
}
}
}
Retryable Errors
- 408 Request Timeout
- 429 Too Many Requests (with Retry-After)
- 500 Internal Server Error (sometimes)
- 502 Bad Gateway
- 503 Service Unavailable
- 504 Gateway Timeout
Non-Retryable Errors
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 409 Conflict
- 422 Unprocessable Entity
Multi-Language Support
Support error messages in multiple languages:
GET /users/invalid
Accept-Language: es
Response: 404 Not Found
Content-Language: es
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Usuario con ID 'invalid' no encontrado"
}
}
Always include code so clients can implement their own translations.
Best Practices
- Use standard HTTP status codes - Don't return 200 for errors
- Include machine-readable codes - Error codes for client logic
- Provide human-readable messages - Clear explanations
- Be specific but safe - Don't expose sensitive information
- Include request ID - For tracking and debugging
- Document all errors - Every possible error for each endpoint
- Be consistent - Same format across all endpoints
- Help clients retry - Indicate if error is retryable
- Validate early - Return validation errors immediately
- Log errors server-side - Track errors for monitoring
Anti-Patterns
Avoid these mistakes:
- Generic error messages - "Error occurred" without details
- Exposing stack traces - Security risk
- Inconsistent error format - Different structure per endpoint
- Missing error codes - Only human-readable messages
- Wrong status codes - Returning 200 with error in body
- No request ID - Makes debugging impossible
- Undocumented errors - Clients don't know what to expect
- Too much information - Exposing internal implementation