API Error Handling & Status Codes: Complete Guide
Proper error handling is crucial for building robust APIs. This guide covers HTTP status codes, error response formats, and best practices for communicating errors to clients.
Why Error Handling Matters
- Helps clients understand what went wrong
- Enables proper error handling in applications
- Improves user experience
- Aids in debugging and monitoring
- Makes APIs more predictable
HTTP Status Codes
1xx Informational
100 - Continue # Client can continue with request
101 - Switching Protocols # Switching to different protocol
2xx Success
200 - OK # Request succeeded
201 - Created # Resource successfully created
202 - Accepted # Request accepted for processing
204 - No Content # Success, no content to return
3xx Redirection
301 - Moved Permanently # Resource moved permanently
302 - Found # Temporary redirect
304 - Not Modified # Cached response still valid
4xx Client Errors
400 - Bad Request # Invalid request syntax
401 - Unauthorized # Authentication required
403 - Forbidden # Access denied
404 - Not Found # Resource doesn't exist
405 - Method Not Allowed # HTTP method not supported
408 - Request Timeout # Client took too long
409 - Conflict # Resource conflict
413 - Payload Too Large # Request body too large
415 - Unsupported Media # Invalid content type
422 - Unprocessable # Valid but semantic errors
429 - Too Many Requests # Rate limited
5xx Server Errors
500 - Internal Server # Generic server error
501 - Not Implemented # Feature not implemented
502 - Bad Gateway # Invalid response from upstream
503 - Service Unavailable # Server temporarily unavailable
504 - Gateway Timeout # Upstream timeout
Error Response Format
Basic Error Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request body contains invalid data",
"details": []
}
}
Detailed Error Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"field": "email",
"details": [
{
"field": "email",
"message": "Invalid email format",
"value": "not-an-email"
},
{
"field": "password",
"message": "Password must be at least 8 characters",
"value": "short"
}
],
"requestId": "req_abc123xyz"
}
}
Error with Help
{
"error": {
"code": "AUTHENTICATION_FAILED",
"message": "Invalid API key",
"help": "Provide a valid API key in the Authorization header",
"docs": "https://api.example.com/docs/auth"
}
}
Implementation Examples
Python (FastAPI)
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class APIException(Exception):
def __init__(self, code: str, message: str, status_code: int = 400, details: dict = None):
self.code = code
self.message = message
self.status_code = status_code
self.details = details or {}
@app.exception_handler(APIException)
async def api_exception_handler(request: Request, exc: APIException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"details": exc.details
}
}
)
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = db.get_user(user_id)
if not user:
raise APIException("NOT_FOUND", "User not found", 404)
return user
@app.post("/users")
async def create_user(user_data: UserSchema):
errors = validate(user_data)
if errors:
raise APIException("VALIDATION_ERROR", "Invalid data", 400, errors)
return db.create_user(user_data)
Node.js (Express)
// Custom error class
class APIError extends Error {
constructor(code, message, statusCode = 400, details = null) {
super(message);
this.code = code;
this.statusCode = statusCode;
this.details = details;
}
}
// Error handler middleware
const errorHandler = (err, req, res, next) => {
console.error('Error:', err);
const response = {
error: {
code: err.code || 'INTERNAL_ERROR',
message: err.message || 'An unexpected error occurred',
}
};
if (err.details) {
response.error.details = err.details;
}
if (err.statusCode === 500) {
response.error.requestId = req.id; // Add request ID for debugging
}
res.status(err.statusCode || 500).json(response);
};
// Route handlers
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
throw new APIError('NOT_FOUND', 'User not found', 404);
}
res.json(user);
} catch (error) {
next(error);
}
});
app.use(errorHandler);
Validation Errors
Form Validation
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"errors": [
{
"field": "email",
"message": "Email is required",
"code": "REQUIRED"
},
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
},
{
"field": "password",
"message": "Password must be at least 8 characters",
"code": "MIN_LENGTH",
"min": 8
},
{
"field": "age",
"message": "Age must be between 18 and 120",
"code": "OUT_OF_RANGE",
"min": 18,
"max": 120
}
]
}
}
Nested Validation
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid nested object",
"errors": [
{
"field": "address.street",
"message": "Street is required"
},
{
"field": "address.coordinates.lat",
"message": "Latitude must be between -90 and 90",
"code": "OUT_OF_RANGE"
}
]
}
}
Custom Error Codes
Error Code Convention
const ErrorCodes = {
// Authentication
AUTH_REQUIRED: 'AUTH_REQUIRED',
AUTH_INVALID: 'AUTH_INVALID',
AUTH_EXPIRED: 'AUTH_EXPIRED',
AUTH_INSUFFICIENT: 'AUTH_INSUFFICIENT',
// Validation
VALIDATION_ERROR: 'VALIDATION_ERROR',
REQUIRED_FIELD: 'REQUIRED_FIELD',
INVALID_FORMAT: 'INVALID_FORMAT',
OUT_OF_RANGE: 'OUT_OF_RANGE',
// Resources
NOT_FOUND: 'NOT_FOUND',
ALREADY_EXISTS: 'ALREADY_EXISTS',
CONFLICT: 'CONFLICT',
// Rate Limiting
RATE_LIMITED: 'RATE_LIMITED',
// Server
INTERNAL_ERROR: 'INTERNAL_ERROR',
EXTERNAL_SERVICE_ERROR: 'EXTERNAL_SERVICE_ERROR'
};
Usage
throw new APIError(
ErrorCodes.VALIDATION_ERROR,
'Invalid request',
400,
[{ field: 'email', message: 'Required' }]
);
Best Practices
Do’s
// Good: Consistent error structure
{
"error": {
"code": "ERROR_CODE",
"message": "Human readable message"
}
}
// Good: Include request ID for debugging
{
"error": {
"code": "INTERNAL_ERROR",
"message": "Something went wrong",
"requestId": "req_abc123"
}
}
// Good: Provide helpful messages
{
"error": {
"code": "",
"messageAUTH_INVALID": "Invalid API key",
"help": "Get a new key at https://api.example.com/keys"
}
}
Don’ts
// Bad: Different structures for same errors
{ "message": "Error" }
{ "error": "Error" }
{ "status": "error", "msg": "Error" }
// Bad: Exposing internal errors
{ "error": { "message": "Database connection failed" } }
// Bad: Generic messages
{ "error": { "message": "An error occurred" } }
Error Handling by Status Code
400 Bad Request
{
"error": {
"code": "BAD_REQUEST",
"message": "Invalid JSON in request body",
"details": [{
"field": "body",
"message": "Expected object, got array"
}]
}
}
401 Unauthorized
{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required",
"help": "Include Authorization header with Bearer token"
}
}
403 Forbidden
{
"error": {
"code": "FORBIDDEN",
"message": "Insufficient permissions",
"required": ["admin:write"],
"current": ["user:read"]
}
}
404 Not Found
{
"error": {
"code": "NOT_FOUND",
"message": "User with ID 123 not found",
"resource": "User",
"resourceId": "123"
}
}
429 Too Many Requests
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests",
"retryAfter": 60,
"limit": 100,
"remaining": 0,
"resetAt": "2024-01-01T12:00:00Z"
}
}
500 Internal Server Error
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"requestId": "req_abc123"
}
}
External Resources
- MDN HTTP Status Codes
- RFC 7807 - Problem Details for HTTP APIs
- REST API Error Handling Best Practices
Comments