Skip to main content

API Error Handling & Status Codes: Complete Guide

Created: February 26, 2026 Larry Qu 6 min read

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

Status Code Selection

flowchart TD
    R[Incoming Request] --> A{Valid Request?}
    A -->|No| B{Client Error?}
    A -->|Yes| C{Authorized?}
    C -->|No| D[401 Unauthorized]
    C -->|Yes| E{Permitted?}
    E -->|No| F[403 Forbidden]
    E -->|Yes| G{Resource Exists?}
    G -->|No| H[404 Not Found]
    G -->|Yes| I{Method Allowed?}
    I -->|No| J[405 Method Not Allowed]
    I -->|Yes| K{Acceptable?}
    K -->|No| L[406 Not Acceptable]
    K -->|Yes| M{Conflict?}
    M -->|Yes| N[409 Conflict]
    M -->|No| O{Valid Data?}
    O -->|No| P[422 Unprocessable]
    O -->|Yes| Q{Rate Limited?}
    Q -->|Yes| R2[429 Too Many Requests]
    Q -->|No| S[2xx Success]

1xx Informational

Code Description Use Case
100 Continue Client can continue sending request body
101 Switching Protocols Upgrading to WebSocket

2xx Success

Code Description Use Case
200 OK Standard successful GET/PUT/PATCH
201 Created Resource created (POST)
202 Accepted Request queued for async processing
204 No Content DELETE success, no body needed

3xx Redirection

Code Description Use Case
301 Moved Permanently Resource URL changed permanently
302 Found Temporary redirect
304 Not Modified Cached response still valid (ETag)

4xx Client Errors

Code Description Use Case
400 Bad Request Malformed syntax or missing parameters
401 Unauthorized No valid authentication provided
403 Forbidden Authenticated but insufficient permissions
404 Not Found Resource does not exist
405 Method Not Allowed HTTP method not supported on this endpoint
408 Request Timeout Client did not send request in time
409 Conflict Resource state conflicts (duplicate, stale version)
413 Payload Too Large Request body exceeds server limit
415 Unsupported Media Type Invalid Content-Type header
422 Unprocessable Entity Valid syntax but semantic validation errors
429 Too Many Requests Rate limit exceeded

5xx Server Errors

Code Description Use Case
500 Internal Server Error Generic unexpected failure
501 Not Implemented Feature not supported
502 Bad Gateway Upstream service returned invalid response
503 Service Unavailable Temporary overload or maintenance
504 Gateway Timeout Upstream service did not respond in time

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": "AUTH_INVALID",
    "message": "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"
  }
}

Conclusion

Good error handling makes your API predictable and debuggable. Use the right HTTP status code for each situation—let the decision flow diagram guide your choice. Return a consistent error body with a machine-readable code, a human-readable message, and field-level details where applicable. Never expose stack traces or internal implementation details in error responses.

Log errors with enough context (request ID, endpoint, user, error code) to debug without reproducing. For 500-level errors, always include a request ID so users can reference it in support tickets.

For broader API design guidance, see the REST API Design Best Practices guide. For authentication-specific error patterns, see the API Authentication Methods guide. For rate limiting error responses, see the API Rate Limiting Strategies guide.

Resources

Comments

Share this article

Scan to read on mobile

👍 Was this article helpful?