Skip to main content

API Error Handling: Status Codes & Error Objects 2026

·APIScout Team
Share:

How to Handle API Errors: Status Codes and Error Objects

Bad error handling is the number one developer experience complaint about APIs. Generic "Something went wrong" messages waste debugging time. Missing status codes break client error handling. Inconsistent error formats require per-endpoint error parsing. Here's how to handle errors properly on both sides.

The stakes are higher than they appear. A developer integrating your API will encounter an error within the first 30 minutes — it's nearly inevitable during the setup and testing phase. How your API responds to that first error determines whether they can self-serve a fix or have to email support. Clear, actionable error messages with machine-readable codes and links to documentation turn a frustrating experience into a solvable one. The APIs with the best developer reputations (Stripe, Twilio, GitHub) all invest heavily in their error response design.

HTTP Status Codes: The Complete Guide

2xx — Success

CodeNameWhen to Use
200OKSuccessful GET, PUT, PATCH, DELETE with response body
201CreatedSuccessful POST that creates a resource (include Location header)
202AcceptedRequest accepted for async processing (not yet completed)
204No ContentSuccessful DELETE or PUT with no response body

4xx — Client Errors

CodeNameWhen to Use
400Bad RequestMalformed request (invalid JSON, missing required fields)
401UnauthorizedMissing or invalid authentication credentials
403ForbiddenAuthenticated but not authorized for this resource/action
404Not FoundResource doesn't exist at this URL
405Method Not AllowedHTTP method not supported for this endpoint
409ConflictResource state conflict (duplicate, version mismatch)
410GoneResource existed but has been permanently deleted
422Unprocessable EntityRequest is well-formed but semantically invalid
429Too Many RequestsRate limit exceeded (include Retry-After header)

5xx — Server Errors

CodeNameWhen to Use
500Internal Server ErrorUnexpected server failure (bug, unhandled exception)
502Bad GatewayUpstream service returned an invalid response
503Service UnavailableServer is temporarily overloaded or in maintenance
504Gateway TimeoutUpstream service didn't respond in time

The Minimum Set

If you only use a few, use these: 200, 201, 204, 400, 401, 403, 404, 409, 422, 429, 500, 503.

Error Response Format

The Standard Error Object

{
  "error": {
    "type": "validation_error",
    "code": "invalid_parameter",
    "message": "The 'email' field must be a valid email address.",
    "param": "email",
    "request_id": "req_abc123def456",
    "doc_url": "https://api.example.com/docs/errors#invalid_parameter"
  }
}

With Multiple Validation Errors

{
  "error": {
    "type": "validation_error",
    "message": "Request validation failed.",
    "errors": [
      {
        "field": "email",
        "code": "invalid_format",
        "message": "Must be a valid email address."
      },
      {
        "field": "password",
        "code": "too_short",
        "message": "Must be at least 8 characters.",
        "metadata": { "min_length": 8, "actual_length": 5 }
      }
    ],
    "request_id": "req_abc123"
  }
}

Essential Fields

FieldPurposeRequired
typeError category (validation_error, authentication_error, rate_limit_error)Yes
codeMachine-readable error code (invalid_parameter, not_found, rate_limited)Yes
messageHuman-readable explanationYes
request_idUnique ID for debugging and supportYes
param / fieldWhich parameter/field caused the errorFor validation errors
doc_urlLink to documentation about this errorRecommended
metadataAdditional context (limits, allowed values, etc.)Optional

How Top APIs Handle Errors

Stripe

{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "decline_code": "insufficient_funds",
    "message": "Your card has insufficient funds.",
    "param": "source",
    "charge": "ch_abc123"
  }
}

Stripe's errors include the relevant object ID (charge), decline codes, and the specific parameter that caused the error.

GitHub

{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "Issue",
      "field": "title",
      "code": "missing_field"
    }
  ],
  "documentation_url": "https://docs.github.com/rest"
}

GitHub includes the resource type and a documentation URL.

Twilio

{
  "code": 20003,
  "message": "Permission denied",
  "more_info": "https://www.twilio.com/docs/errors/20003",
  "status": 403
}

Twilio uses numeric error codes with a direct link to detailed error documentation.

Server-Side Error Handling Implementation

// Centralized error handler middleware (Express)
import { Request, Response, NextFunction } from 'express';
import { ZodError } from 'zod';

interface AppError extends Error {
  statusCode?: number;
  code?: string;
}

export function errorHandler(
  err: AppError,
  req: Request,
  res: Response,
  _next: NextFunction,
) {
  const requestId = req.headers['x-request-id'] as string ?? crypto.randomUUID();

  // Zod validation errors
  if (err instanceof ZodError) {
    return res.status(422).json({
      error: {
        type: 'validation_error',
        code: 'invalid_request',
        message: 'Request validation failed.',
        request_id: requestId,
        errors: err.errors.map(e => ({
          field: e.path.join('.'),
          code: e.code,
          message: e.message,
        })),
      },
    });
  }

  // Known application errors
  if (err.statusCode) {
    return res.status(err.statusCode).json({
      error: {
        type: err.name ?? 'api_error',
        code: err.code ?? 'error',
        message: err.message,
        request_id: requestId,
      },
    });
  }

  // Unknown errors — log full detail, return generic response
  console.error({ requestId, error: err.message, stack: err.stack });
  return res.status(500).json({
    error: {
      type: 'internal_error',
      code: 'internal_server_error',
      message: 'An unexpected error occurred.',
      request_id: requestId,
    },
  });
}

The key insight: the requestId appears in both the error response body and your server logs. When a user reports a problem and provides their request ID (from your error message or your app's error UI), you can immediately find the full server-side log entry for that specific request.

Client-Side Error Handling

The Error Handling Hierarchy

  1. Network errors — no response received (timeout, DNS failure, connection refused)
  2. HTTP errors — response received with error status code
  3. Application errors — 200 response but with error in the body (some APIs do this)

Pattern: Retry Strategy

Error TypeShould Retry?Strategy
400 (Bad Request)NoFix the request
401 (Unauthorized)NoRe-authenticate
403 (Forbidden)NoCheck permissions
404 (Not Found)NoResource doesn't exist
409 (Conflict)MaybeRe-read and retry
429 (Rate Limited)YesWait Retry-After seconds
500 (Server Error)YesExponential backoff
502 (Bad Gateway)YesExponential backoff
503 (Unavailable)YesWait Retry-After or backoff
Network timeoutYesExponential backoff

Pattern: Error Classification

Classify errors into three categories for your application:

  1. Retryable — 429, 500, 502, 503, 504, network timeouts → retry with backoff
  2. Fixable — 400, 401, 422 → user can fix the input
  3. Terminal — 403, 404, 409 → can't be fixed by retrying or changing input

Client-Side Error UX

The error handling chain extends to your application UI. When an API call fails, the user experience depends on how your application surfaces the error:

  • Retryable errors (5xx, 429): Show a "Something went wrong, trying again..." message. Don't show raw error codes to users. Implement automatic retry with user-visible feedback (spinner, progress indicator).
  • Fixable errors (4xx): Show specific guidance. "Invalid email address" is better than "Bad Request". Extract the message field from the error response for user-facing display — API providers write them to be human-readable.
  • Auth errors (401): Redirect to login or refresh the token silently. Users shouldn't see "Unauthorized" — they should see a login prompt.
  • Not found (404): Show a friendly "This [thing] doesn't exist" message, not the raw 404. Provide navigation back to a valid state.

Never expose request_id, error codes, or technical details in the user-facing UI — those belong in your application logs and error reporting, not in a toast notification that the user sees.

Common Mistakes

MistakeImpactFix
200 for everythingClients can't distinguish success/failure by status codeUse proper HTTP status codes
Plain text error messagesNo machine parsingJSON error objects
No request IDDebugging requires reproducing the issueInclude request_id in every error
Generic "Server Error"No debugging informationSpecific error codes and messages
Stack traces in productionSecurity vulnerability (leaks internals)Log server-side, return generic 500
Missing Retry-After on 429Clients retry immediately, making it worseAlways include Retry-After
Different error formats per endpointClient needs per-endpoint error handlingConsistent error schema
Undocumented error codesDevelopers can't self-diagnosePublish error reference in API docs

Building a Typed Error Handler (TypeScript)

For API consumers, centralizing error handling in one place prevents ad-hoc error checking across every API call:

// lib/api-client.ts
export class APIError extends Error {
  constructor(
    message: string,
    public readonly status: number,
    public readonly code: string,
    public readonly requestId: string,
    public readonly retryable: boolean,
    public readonly details?: unknown,
  ) {
    super(message);
    this.name = 'APIError';
  }
}

async function apiRequest<T>(url: string, options: RequestInit = {}): Promise<T> {
  const response = await fetch(url, {
    ...options,
    headers: { 'Content-Type': 'application/json', ...options.headers },
  });

  if (!response.ok) {
    const body = await response.json().catch(() => ({}));
    const error = body.error ?? body;
    const isRetryable = [429, 500, 502, 503, 504].includes(response.status);

    throw new APIError(
      error.message ?? `HTTP ${response.status}`,
      response.status,
      error.code ?? `http_${response.status}`,
      error.request_id ?? response.headers.get('x-request-id') ?? '',
      isRetryable,
      error.details ?? error.errors,
    );
  }

  return response.json();
}

// Automatic retry for retryable errors
async function apiRequestWithRetry<T>(
  url: string,
  options: RequestInit = {},
  maxRetries = 3,
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await apiRequest<T>(url, options);
    } catch (err) {
      if (!(err instanceof APIError) || !err.retryable || attempt === maxRetries) throw err;
      const delay = err.status === 429
        ? parseInt(/* Retry-After header */ '1') * 1000
        : Math.pow(2, attempt) * 500;
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error('Unreachable');
}

Documenting Errors for API Consumers

Your error codes are part of your API's public contract. Document them in your API reference with the same care you document endpoints.

For each error code in your API, document:

  • When it occurs — the specific conditions that trigger it
  • How to fix it — what the caller should do differently
  • Whether to retry — is this a transient error or a permanent one?
  • Example response — copy-paste the JSON so developers know exactly what to expect

Stripe's error documentation is the gold standard: every error code has its own page with trigger conditions, resolution steps, and code examples. For smaller APIs, even a simple error reference table significantly reduces support ticket volume by enabling developers to self-diagnose.

## Error Reference

### invalid_parameter (400)
The request contains an invalid parameter value.

**When it occurs:** A parameter failed validation — wrong type, out of range, or invalid format.
**Resolution:** Check the `param` field to identify which parameter caused the error.
**Retry:** No — fix the request before retrying.

**Example:**
```json
{
  "error": {
    "type": "validation_error",
    "code": "invalid_parameter",
    "message": "The 'email' field must be a valid email address.",
    "param": "email",
    "request_id": "req_abc123"
  }
}

rate_limited (429)

Request rate limit exceeded.

When it occurs: You've exceeded the API rate limit for your plan. Resolution: Wait for the Retry-After header value (in seconds) before retrying. Retry: Yes — after the specified delay.


## Monitoring API Errors in Production

Errors you don't know about are bugs you can't fix. Two monitoring patterns that catch API error spikes before users report them:

**Error rate alerts**: Alert when the 5xx error rate exceeds 1% of requests in a 5-minute window. Tools like Datadog, Sentry, or Prometheus can track `http_response_status_code` as a metric and alert on sudden increases.

**Request ID in your logs**: Log the `request_id` from every API error response alongside your application error. When a user reports a problem, you can correlate your logs with the API provider's server-side logs using the request ID — dramatically reducing the time to diagnose third-party API issues.

```typescript
// Log API errors with full context for debugging
catch (err) {
  if (err instanceof APIError) {
    logger.error('API request failed', {
      url,
      status: err.status,
      code: err.code,
      requestId: err.requestId,  // Reference for support tickets
      retryable: err.retryable,
      userId: currentUser?.id,
    });
  }
}

Methodology

Error format examples and patterns sourced from Stripe, GitHub, and Twilio official API documentation. Status code guidance based on RFC 7231 (HTTP/1.1 Semantics) and RFC 9110 (HTTP Semantics, 2022). Retry strategy recommendations based on published SRE practices from Google, Stripe engineering blog, and AWS Well-Architected Framework. Code examples tested against Node.js 20 LTS with Express 4.x and Zod 3.x.

Designing API error handling? Explore API best practices and tools on APIScout — architecture guides, comparisons, and developer resources.

Related: API Error Handling Patterns for Production Applications, How AI Is Transforming API Design and Documentation, API Breaking Changes Without Breaking Clients

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.