Skip to content
C Codeloom
REST APIs

REST API Error Handling Conventions

Design clear, consistent error responses for REST APIs using HTTP status codes, problem details, and error envelopes that clients can actually handle.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • Choosing the right HTTP status codes
  • Designing an error envelope
  • Using RFC 7807 Problem Details
  • Mapping domain errors to responses
  • Avoiding leaking internals to clients

Prerequisites

  • Familiar with HTTP

What and Why

A REST API does two things when something goes wrong: it tells the client what happened, and gives just enough context to recover. Inconsistent error formats are one of the most common sources of friction for API consumers. A team that nails error handling early saves itself thousands of support tickets later.

Good error responses are predictable, machine-readable, and human-friendly. Bad ones return 200 OK with {"error": "something broke"} and a stack trace in the message.

Mental Model

Think of an error response as three layers:

  1. The HTTP status code (the category).
  2. A machine-readable error code (the specific reason).
  3. A human-readable message (for logs and debugging).

The status code tells a generic HTTP client what to do. The error code lets your SDK or frontend handle specific cases. The message helps an engineer reading a log line at 3am.

Client request
   |
   v
+----------------+
| HTTP status    |  400 / 401 / 404 / 409 / 422 / 500
+----------------+
| Error code     |  "user.email_taken"
+----------------+
| Message + meta |  "Email already registered", trace_id
+----------------+
Error response layering

Hands-on Example

Let’s design responses for a sign-up endpoint. Start with the status code:

  • 400 for malformed JSON or missing fields.
  • 409 when the email is already in use.
  • 422 when fields are present but semantically invalid (weak password).
  • 429 when the client is rate limited.
  • 500 only for true server faults.

A practical envelope:

{
  "error": {
    "code": "user.email_taken",
    "message": "An account with this email already exists.",
    "details": [
      { "field": "email", "issue": "duplicate" }
    ],
    "trace_id": "8f3c2a..."
  }
}

If you prefer a standard, use RFC 7807 Problem Details:

{
  "type": "https://api.example.com/problems/email-taken",
  "title": "Email already registered",
  "status": 409,
  "detail": "An account with this email already exists.",
  "instance": "/v1/users",
  "trace_id": "8f3c2a..."
}

Set Content-Type: application/problem+json so caches and gateways recognize it.

For validation errors, return all problems at once, not one per request:

{
  "error": {
    "code": "validation_failed",
    "message": "Some fields are invalid.",
    "details": [
      { "field": "email", "issue": "format" },
      { "field": "password", "issue": "too_short" }
    ]
  }
}

Common Pitfalls

Returning 200 for everything. This breaks proxies, retries, and monitoring. If something failed, use a 4xx or 5xx status.

Inconsistent shapes. One endpoint returns {error: "..."}, another returns {errors: [...]}, another returns {message: "..."}. Pick one envelope and document it.

Leaking stack traces. Never return raw exception text in production. Log it server-side, return a trace_id the client can quote.

Using 401 vs 403 incorrectly. 401 means “we do not know who you are.” 403 means “we know who you are and you cannot do this.”

Overusing 500. A duplicate email is not a server error. Reserve 500 for unhandled exceptions and infrastructure failures.

Mixing localization into the API. Return a stable code and let the client localize the message. Backends should not own UI copy.

Practical Tips

  • Document every error code in your API reference, with cause and remediation.
  • Include a trace_id or request_id header on every response, success or failure. Echo it in error bodies.
  • Use a single error middleware that converts thrown exceptions into your envelope. Domain code throws, infrastructure formats.
  • Version your error codes the same way you version endpoints. Removing or renaming a code is a breaking change.
  • For paginated or batch operations, decide upfront: fail the whole batch, or return per-item errors with 207 Multi-Status style payloads.
  • Add Retry-After headers on 429 and 503 so well-behaved clients back off correctly.
  • Log the full error server-side with the same trace_id you returned. Searching production by trace ID should land on one log line.

Wrap-up

Consistent errors are a feature. Pick a clear envelope (custom or RFC 7807), map status codes deliberately, and treat error codes as a public contract. Your future on-call self and every client developer who integrates against your API will thank you.

The goal is not zero errors. It is predictable errors that clients can reason about without reading your source code.