Beginner20 min read

Error Handling in APIs

Master API error response design, consistent error formats, validation errors, error codes, and best practices from real-world APIs like Stripe and GitHub.

Why Error Handling Matters

Errors are inevitable. Users will send invalid data. Databases will go down. Third-party APIs will timeout. Networks will fail. The difference between a good API and a great API is how it handles these failures.

A well-designed error response tells the client:

  1. What went wrong — A human-readable message
  2. Why it went wrong — Specific details (which field failed validation, what constraint was violated)
  3. How to fix it — Actionable guidance
  4. What category of error — Client mistake (4xx) or server failure (5xx)

A poorly-designed error response leaves the client guessing:

json
{
  "error": "Something went wrong"
}

What went wrong? Is it my fault or yours? Which field is invalid? What should I do?

Compare this to a well-designed error:

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request data",
    "details": [
      {
        "field": "email",
        "message": "Email must be a valid email address",
        "value": "not-an-email"
      },
      {
        "field": "age",
        "message": "Age must be at least 18",
        "value": 15
      }
    ]
  }
}

Now the client knows exactly what is wrong and how to fix it. This is the standard we should aim for.

Consistent Error Response Format

The first rule of error handling is consistency. Every error response, regardless of type, should follow the same structure. This makes errors predictable and easy to handle programmatically.

Here is a recommended error response format:

json
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable summary",
    "details": [ /* Optional: field-level errors */ ]
  }
}

The error object:

  • Always nest the error information under an error key (not at the root)
  • This makes it clear that this is an error response, not a success response

The code field:

  • A machine-readable error identifier (e.g., VALIDATION_ERROR, RESOURCE_NOT_FOUND, RATE_LIMIT_EXCEEDED)
  • Use SCREAMING_SNAKE_CASE
  • Error codes allow clients to handle specific errors programmatically
  • Different from HTTP status codes (one status code can have multiple error codes)

The message field:

  • A human-readable summary of what went wrong
  • Should be clear and actionable
  • Can be displayed to end users or logged for debugging

The details field (optional):

  • For validation errors, provide field-level details
  • Array of objects with field, message, and optionally value
  • Helps clients highlight specific form fields with errors

Examples:

404 Not Found:

json
HTTP/1.1 404 Not Found
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "User with ID 123 does not exist"
  }
}

401 Unauthorized:

json
HTTP/1.1 401 Unauthorized
{
  "error": {
    "code": "INVALID_TOKEN",
    "message": "Authentication token is invalid or expired"
  }
}

500 Internal Server Error:

json
HTTP/1.1 500 Internal Server Error
{
  "error": {
    "code": "INTERNAL_SERVER_ERROR",
    "message": "An unexpected error occurred. Please try again later."
  }
}

Notice how each error, regardless of status code, follows the same structure. This consistency is crucial.

Validation Errors with Field-Level Details

Validation errors are special because they often involve multiple fields failing multiple constraints. A user might submit a form with an invalid email, a password that is too short, and a missing required field — all in one request.

For these cases, use a 422 Unprocessable Entity status code and provide field-level details in the details array:

json
HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Email is required"
      },
      {
        "field": "password",
        "message": "Password must be at least 8 characters",
        "value": "abc"
      },
      {
        "field": "age",
        "message": "Age must be a number",
        "value": "not-a-number"
      }
    ]
  }
}

Each item in the details array has:

  • field — The name of the field that failed (matches the request body field name)
  • message — A human-readable error message specific to this field
  • value (optional) — The invalid value that was submitted (helpful for debugging)

This structure allows frontend applications to:

  1. Parse the error response
  2. Loop through the details array
  3. Highlight each invalid field in the UI
  4. Display the specific error message next to each field

Why not just return the first error? Some APIs return only the first validation error and require the client to fix it and resubmit to see the next error. This is frustrating for users. If the client submits a form with 5 invalid fields, tell them about all 5 at once, not one at a time.

400 vs 422 for validation errors?

  • 400 Bad Request — The request is malformed (invalid JSON, wrong Content-Type)
  • 422 Unprocessable Entity — The request is well-formed but fails business logic validation

Both are acceptable, but 422 is more precise for validation errors.

Error Categories: Client vs Server Errors

Errors fall into two categories, and it is crucial to distinguish between them:

Client Errors (4xx) — The client made a mistake These errors are caused by invalid input, missing authentication, lack of permissions, or requesting a resource that doesn't exist. The client should fix the request and try again.

Common client errors:

  • 400 Bad Request — Malformed request syntax
  • 401 Unauthorized — Missing or invalid authentication
  • 403 Forbidden — Authenticated but not authorized
  • 404 Not Found — Resource doesn't exist
  • 422 Unprocessable Entity — Validation failed
  • 429 Too Many Requests — Rate limit exceeded

When to use: The error is the client's fault and they can fix it.

Server Errors (5xx) — The server failed These errors are caused by bugs, crashes, database failures, or third-party service outages. The client did nothing wrong. The request might succeed if retried later.

Common server errors:

  • 500 Internal Server Error — Unexpected error (crashed, uncaught exception)
  • 502 Bad Gateway — Upstream service returned an invalid response
  • 503 Service Unavailable — Server is temporarily down (maintenance, overload)
  • 504 Gateway Timeout — Upstream service didn't respond in time

When to use: The error is the server's fault and the client cannot fix it.

Critical distinction: If a client sends {age: "not-a-number"}, that is a client error (422). If the database crashes while processing a valid request, that is a server error (500).

Clients can retry 5xx errors (with exponential backoff), but retrying 4xx errors without fixing the request is pointless.

Real-World Examples: Stripe, GitHub, Twilio

Let's look at how industry-leading APIs handle errors.

Stripe (Payment API) Stripe has one of the best-designed error responses in the industry:

json
{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "message": "Your card was declined.",
    "param": "exp_year",
    "decline_code": "generic_decline"
  }
}

Notice:

  • Clear type categorization (card_error, invalid_request_error, api_error)
  • Specific code for programmatic handling
  • Human-readable message
  • param field to indicate which parameter caused the error
  • Additional context (decline_code) for payment-specific errors

GitHub (REST API) GitHub uses a clean, consistent format:

json
{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "Issue",
      "field": "title",
      "code": "missing_field"
    }
  ],
  "documentation_url": "https://docs.github.com/rest/reference/issues#create-an-issue"
}

Notice:

  • errors array for field-level details
  • resource and field to pinpoint the problem
  • documentation_url to help developers understand the API

Twilio (SMS API) Twilio uses a simple, effective format:

json
{
  "code": 21211,
  "message": "The 'To' number is not a valid phone number.",
  "more_info": "https://www.twilio.com/docs/errors/21211",
  "status": 400
}

Notice:

  • Numeric error codes (legacy, but consistent with their docs)
  • more_info URL linking to detailed error documentation
  • status field (redundant with HTTP status, but explicit)

Common patterns:

  1. All use structured, consistent error formats
  2. All include machine-readable codes
  3. All include human-readable messages
  4. All provide additional context (field, param, resource)
  5. Many link to documentation for more help

These APIs are used by millions of developers precisely because their errors are predictable, informative, and actionable.

A user submits a form to create an account, but the email field is missing and the password is too short. What should the API return?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.