Skip to content

Error Handling in REST APIs — Status Codes and Responses Guide

DodaTech Updated 2026-06-23 9 min read

In this tutorial, you'll learn about Error Handling in REST APIs. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Error handling in REST APIs is the practice of returning consistent, informative HTTP status codes and structured error responses that help client developers understand and handle failures without guessing what went wrong.

What You'll Learn

You will learn proper HTTP status code selection, consistent error response formats, global error handler implementation, validation error reporting, and error logging strategies for production APIs.

Why Error Handling Matters

Poor error handling is the number one complaint from API consumers. Inconsistent error formats force developers to parse response bodies manually. Generic 500 errors hide real problems. Good error handling reduces integration time by 50 percent, improves developer experience, and makes debugging production issues faster.

Real-World Use

DodaTech APIs follow a consistent error response format across all products. Doda Browser sync API returns structured errors that the browser can display to users, DodaZIP update service includes retry hints in error responses, and Durga Antivirus Pro uses detailed error codes for SIEM integration.

Error Handling Learning Path

flowchart LR
  A[REST API Design] --> B[Status Code Selection]
  B --> C[Error Response Format]
  C --> D[Global Error Handler]
  D --> E[Validation Errors]
  E --> F[Logging & Monitoring]
  F --> G[Client SDK Generation]
  B:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px

Prerequisites

Understand RESTful Api Design Best Practices and HTTP Protocol Basics. Familiarity with JavaScript Basics or Python Basics is required for implementation examples.

HTTP Status Code Guide

2xx Success Codes

Code Meaning When to Use
200 OK Request succeeded GET, PUT, PATCH success
201 Created Resource created POST success
204 No Content Success, no body DELETE success
202 Accepted Request accepted asynchronously Long-running operations

3xx Redirection

Code Meaning When to Use
301 Moved Permanently Resource URL changed permanently API Migration
302 Found Temporary redirect Load Balancing
303 See Other Redirect to another URL After POST (PRG pattern)
304 Not Modified Resource not changed (cached) Conditional GET with ETag

4xx Client Errors

Code Meaning When to Use
400 Bad Request Malformed request syntax Invalid JSON, missing required fields
401 Unauthorized Authentication required Missing or invalid auth token
403 Forbidden No permission Authenticated but not authorized
404 Not Found Resource does not exist Invalid resource ID
405 Method Not Allowed HTTP method not supported POST on read-only endpoint
409 Conflict Resource State conflict Duplicate resource, stale version
410 Gone Resource deleted permanently Soft-deleted resources
422 Unprocessable Entity Validation error Invalid field values, type errors
429 Too Many Requests Rate limit exceeded Client exceeded API limits

5xx Server Errors

Code Meaning When to Use
500 Internal Server Error Unexpected server failure Unhandled exceptions
502 Bad Gateway Upstream service failed Proxy/gateway errors
503 Service Unavailable Server temporarily overloaded Maintenance, traffic spikes
504 Gateway Timeout Upstream service timed out Slow dependencies

Consistent Error Response Format

Every error response should follow the same structure:

{
  "status": 400,
  "error": "validation_error",
  "message": "Validation failed for the request",
  "details": [
    {
      "field": "email",
      "code": "invalid_email",
      "message": "The provided email address is not valid",
      "value": "not-an-email]
    }
  ],
  "requestId": "req_abc123",
  "documentationUrl": "https://docs.dodatech.com/api/errors#validation_error",
  "timestamp": "2026-06-23T10:00:00Z"
}

Error Response Fields

Field Required Description
status Yes HTTP status code
error Yes Machine-readable error code
message Yes Human-readable error message
details No Array of field-level errors
requestId Yes Unique request identifier for debugging
documentationUrl No Link to error documentation
timestamp Yes When the error occurred

Machine-Readable Error Codes

const ERROR_CODES = {
  VALIDATION_ERROR: "validation_error",
  NOT_FOUND: "not_found",
  UNAUTHORIZED: "unauthorized",
  FORBIDDEN: "forbidden",
  CONFLICT: "conflict",
  RATE_LIMITED: "rate_limited",
  INTERNAL_ERROR: "internal_error",
  SERVICE_UNAVAILABLE: "service_unavailable",
  DEPENDENCY_FAILURE: "dependency_failure",
  INVALID_STATE: "invalid_state"
};

Global Error Handler Implementation

Express.js Global Error Handler

// Create a custom error class
class ApiError extends Error {
  constructor(statusCode, error, message, details = null) {
    super(message);
    this.statusCode = statusCode;
    this.error = error;
    this.details = details;
    this.requestId = generateRequestId();
    this.timestamp = new Date().toISOString();
  }
}

// Error factory functions
const Errors = {
  badRequest: (msg, details) =>
    new ApiError(400, "bad_request", msg, details),
  unauthorized: (msg = "Authentication required") =>
    new ApiError(401, "unauthorized", msg),
  forbidden: (msg = "Insufficient permissions") =>
    new ApiError(403, "forbidden", msg),
  notFound: (resource = "Resource") =>
    new ApiError(404, "not_found", `${resource} not found`),
  conflict: (msg) =>
    new ApiError(409, "conflict", msg),
  validationError: (details) =>
    new ApiError(422, "validation_error", "Validation failed", details),
  tooManyRequests: () =>
    new ApiError(429, "rate_limited", "Rate limit exceeded, please try again later"),
  internal: (msg = "Internal server error") =>
    new ApiError(500, "internal_error", msg)
};

// Throw errors in controllers
app.get("/api/users/:id", async (req, res, next) => {
  try {
    const user = await userModel.findById(req.params.id);
    if (!user) {
      throw Errors.notFound("User");
    }
    res.json({ success: true, data: user });
  } catch (error) {
    next(error);
  }
});

// Global error handler middleware
app.use((err, req, res, next) => {
  // Log the error
  console.error(`[${err.requestId}] ${err.statusCode || 500}: ${err.message}`);

  // Determine status code
  const statusCode = err.statusCode || 500;
  const error = err.error || "internal_error";
  const message = statusCode === 500
    ? "An unexpected error occurred"
    : err.message;

  // Build response
  const response = {
    status: statusCode,
    error,
    message,
    requestId: err.requestId || generateRequestId(),
    timestamp: err.timestamp || new Date().toISOString()
  };

  // Include details for validation errors
  if (err.details) {
    response.details = err.details;
  }

  // Include documentation URL for known errors
  if (ERROR_DOCS[error]) {
    response.documentationUrl = ERROR_DOCS[error];
  }

  // In development, include stack trace
  if (process.env.NODE_ENV === "development") {
    response.stack = err.stack;
  }

  res.status(statusCode).json(response);
});

FastAPI Global Error Handler

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
import uuid
from datetime import datetime

app = FastAPI()

class ApiError(Exception):
    def __init__(self, status_code: int, error: str, message: str, details: list = None):
        self.status_code = status_code
        self.error = error
        self.message = message
        self.details = details
        self.request_id = str(uuid.uuid4())
        self.timestamp = datetime.utcnow().isoformat()

@app.exception_handler(ApiError)
async def api_error_handler(request: Request, exc: ApiError):
    response = {
        "status": exc.status_code,
        "error": exc.error,
        "message": exc.message,
        "requestId": exc.request_id,
        "timestamp": exc.timestamp
    }

    if exc.details:
        response["details"] = exc.details

    return JSONResponse(
        status_code=exc.status_code,
        content=response
    )

@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
    return JSONResponse(
        status_code=500,
        content={
            "status": 500,
            "error": "internal_error",
            "message": "An unexpected error occurred",
            "requestId": str(uuid.uuid4()),
            "timestamp": datetime.utcnow().isoformat()
        }
    )

Validation Error Reporting

Field-Level Validation Errors

function validateCreateUser(body) {
  const errors = [];

  if (!body.name || body.name.trim().length < 2) {
    errors.push({
      field: "name",
      code: "too_short",
      message: "Name must be at least 2 characters",
      value: body.name
    });
  }

  if (!body.email || !isValidEmail(body.email)) {
    errors.push({
      field: "email",
      code: "invalid_email",
      message: "A valid email address is required",
      value: body.email
    });
  }

  if (!body.password || body.password.length < 8) {
    errors.push({
      field: "password",
      code: "too_short",
      message: "Password must be at least 8 characters"
    });
  }

  return errors.length > 0 ? errors : null;
}

Expected Validation Error Response

{
  "status": 422,
  "error": "validation_error",
  "message": "Validation failed",
  "details": [
    {
      "field": "name",
      "code": "too_short",
      "message": "Name must be at least 2 characters",
      "value": "]
    },
    {
      "field": "email",
      "code": "invalid_email",
      "message": "A valid email address is required",
      "value": "not-an-email"
    }
  ],
  "requestId": "req_xyz789",
  "timestamp": "2026-06-23T10:00:00Z"
}

Error Logging Strategy

function logError(error, req) {
  const logEntry = {
    requestId: error.requestId,
    timestamp: error.timestamp,
    method: req.method,
    path: req.path,
    statusCode: error.statusCode || 500,
    errorCode: error.error,
    message: error.message,
    userId: req.user?.id,
    ip: req.ip,
    userAgent: req.headers["user-agent"],
    // Stack trace in development only
    ...(Process.env.NODE_ENV === "development" && { Stack: error.Stack })
  };

  // Send to logging service
  logger.error(logEntry);

  // Send critical errors to alerting
  if (error.statusCode >= 500) {
    alertingService.sendAlert(logEntry);
  }
}

Common Errors

  1. Returning 500 for client errors — Validation errors and bad requests should return 4xx codes, not 500. A 500 status tells the client the server is broken when the client actually sent bad data.

  2. Inconsistent error format — Some endpoints return {error: "msg"}, others return {message: "msg"}. Define a single error response format and use it everywhere.

  3. Leaking internal details — Returning database error messages, Stack traces, or internal IP addresses in error responses. Always sanitize error messages for production.

  4. Missing error codes — Using only human-readable messages without machine-readable codes. Clients cannot programmatically handle errors without codes.

  5. No request ID — Returning errors without a way to correlate them to server logs. Always include a unique request ID that developers can provide when reporting issues.

  6. Not documenting error responses — Documenting only successful responses in OpenAPI spec. Every possible error response should be documented with examples.

  7. Empty response bodies on errors — Returning only a status code with no body. Even for 204, you need headers. For all other status codes, include a structured error body.

Practice Questions

  1. What is the difference between 401 Unauthorized and 403 Forbidden?
  2. Why should error responses include a unique request ID?
  3. What status code should you return for a validation error?
  4. How do you implement a global error handler in Express.js?
  5. What fields should a consistent error response include?

Challenge

Implement a comprehensive error handling system for a REST API. Create a custom error class hierarchy, define 15+ machine-readable error codes, implement a global error handler with proper logging, add validation that returns field-level errors, document all error responses in OpenAPI spec, and create an error reference page in the documentation portal.

FAQ

Should I always return a response body for 4xx errors? Yes, always return a structured error body for 4xx and 5xx responses. The only exception is 204 No Content for successful DELETE operations. Even 401 and 403 should include context about the error.

How detailed should error messages be? Detailed enough for a developer to fix the issue without guessing. Include which field failed, why it failed, what was expected, and what was received. Do not include internal implementation details.

Should I include Stack traces in error responses? Only in development environments. Never expose Stack traces in production. Stack traces reveal internal code structure, file paths, and library versions that help attackers.

How do I handle errors from external API calls? Catch the external error, log it with context, and return a 502 Bad Gateway or 503 Service Unavailable with a message that the downstream service is unavailable. Never propagate the external error details.

What is the difference between 400 Bad Request and 422 Unprocessable Entity? 400 is for malformed syntax (invalid JSON, missing Content-Type). 422 is for valid syntax but invalid semantics (email format wrong, age out of range). Both are client errors but distinguish between parsing and validation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro