# Error Response Design

## 1. Why an Ad-Hoc Error Shape Is a Real Cost

An API that returns a different error shape from every endpoint — sometimes a string, sometimes `{error: "..."}`, sometimes `{message: "...", details: [...]}` — forces every client to write bespoke error-handling per endpoint instead of one shared error-handling path. A consistent error shape is what lets a client write **one** generic "show the user this error" function that works for the entire API surface.

## 2. A Minimal Consistent Error Shape

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request could not be processed due to invalid fields.",
    "details": [
      { "field": "email", "issue": "must be a valid email address" },
      { "field": "age", "issue": "must be a positive integer" }
    ]
  }
}
```

Three distinct pieces, each serving a different consumer:

- **`code`** — a stable, machine-readable identifier a client can `switch` on programmatically (never changes across API versions without a deprecation cycle, unlike `message`)
- **`message`** — a human-readable summary, safe to log or show a developer, NOT guaranteed stable wording (may be improved/reworded without being a breaking change)
- **`details`** — structured, field-level breakdown for multi-field validation failures (section 4); omitted entirely for errors that aren't about specific fields

## 3. HTTP Status Code Discipline

The status code and the body's `error.code` serve different audiences: the status code is for **generic HTTP infrastructure** (proxies, caches, monitoring, browsers) that doesn't parse your JSON at all; `error.code` is for **your own client code** that does:

| Status | Use for | NOT for |
|---|---|---|
| `400 Bad Request` | Malformed request the server couldn't even parse/understand (bad JSON, wrong types) | A well-formed request that fails a business rule — see `422` |
| `401 Unauthorized` | No valid identity at all | A valid identity that's simply not allowed to do this — see `403` |
| `403 Forbidden` | Authenticated, but not authorized for this specific action/resource | Missing/invalid credentials — see `401` |
| `404 Not Found` | The resource genuinely doesn't exist | An existing resource the caller isn't allowed to see (some APIs deliberately return `404` here too, to avoid leaking existence — a legitimate design choice, not a mistake) |
| `409 Conflict` | The request conflicts with the resource's current state (e.g. optimistic-concurrency version mismatch, duplicate unique key) | Plain validation failure |
| `422 Unprocessable Entity` | Well-formed, parseable request that fails semantic/business validation (e.g. `age: -5`) | Malformed JSON — that's `400` |
| `429 Too Many Requests` | Rate limit exceeded (see `backend/api-design/07-rate_limiting`) | — |
| `500 Internal Server Error` | An unexpected server-side failure — never anything the client did wrong | Any case where the *client* could have avoided the error by sending a different request |

Collapsing `400` and `422` into "just always send 400" is the single most common status-code sloppiness — it forces every client to parse the error *body* to learn what a status-code check alone should have told them.

## 4. Field-Level Validation Errors

A single request can fail validation on multiple fields at once — reporting only the first one forces the client into a slow fix-one-resubmit-see-the-next loop:

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "3 fields failed validation.",
    "details": [
      { "field": "email", "issue": "required" },
      { "field": "password", "issue": "must be at least 8 characters" },
      { "field": "birthdate", "issue": "must be a date in the past" }
    ]
  }
}
```

Collecting every failure before responding (rather than short-circuiting on the first) is a deliberate design choice tied to `backend/api-design/08-request_validation`'s validation-library patterns — most schema-validation libraries support an "accumulate all errors" mode versus a "fail fast" mode, and the accumulate mode is almost always the right default for API error responses.

## 5. `problem+json` (RFC 7807) — a Real Interoperable Standard

Rather than inventing a bespoke shape from scratch, RFC 7807 defines a standard `application/problem+json` error format some APIs adopt directly (and generic HTTP tooling increasingly recognizes):

```json
{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation Failed",
  "status": 422,
  "detail": "The request could not be processed due to invalid fields.",
  "instance": "/orders",
  "errors": [
    { "field": "email", "issue": "must be a valid email address" }
  ]
}
```

`type` is a URI identifying the *category* of problem (can literally be a link to documentation about it), `title` is a short human summary, `status` mirrors the HTTP status for convenience, `instance` identifies which specific request instance failed. Adopting `problem+json` isn't required — a custom shape like section 2's is equally valid — but it's worth knowing it exists as the closest thing to an industry-standard answer, useful when interoperating with tooling that already expects it.

![A problem+json error body with type, title, status, detail, instance and field errors, with status code as category and type as a stable machine-readable reason](diagrams/01-problem-json.svg)

## 6. Never Leak Internals in Error Messages

```ts
// VULNERABLE: leaks stack trace, DB engine, internal file paths to any client
catch (err) {
  res.status(500).json({ error: { code: "INTERNAL", message: err.stack } });
}

// SAFE: log the full detail server-side, return a generic message to the client
catch (err) {
  logger.error("unhandled error in POST /orders", { error: err }); // full detail, server-only
  res.status(500).json({ error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred." } });
}
```

A `500` response body is visible to anyone who can send the request, including an attacker deliberately triggering errors to map your stack, database engine, or internal paths — the full detail belongs in server-side logs (correlated by a request id the client-facing error can safely include), never in the response body itself.

## Common Pitfalls

1. **A different error shape per endpoint.** Defeats the entire purpose — clients can't write one shared error-handling path (section 1).
2. **Collapsing `400` and `422`.** Forces clients to parse the body to distinguish "you sent garbage" from "you sent well-formed but invalid data" (section 3).
3. **Stopping at the first validation failure.** Forces a slow fix-one-resubmit loop instead of surfacing every problem in one response (section 4).
4. **Leaking stack traces or internal identifiers in `500` bodies.** A live information-disclosure risk, not just an aesthetic one (section 6).
5. **Treating `message` as a stable API contract.** Clients that `if (error.message === "...")` instead of checking `error.code` break the moment wording is improved — `code` is the only field meant to be programmatically stable.

## Quick Reference

```
{ error: { code, message, details? } }   -- stable shape across every endpoint
code     -- machine-readable, stable across wording changes, safe to switch() on
message  -- human-readable, NOT a stable contract
details  -- field-level breakdown, only for multi-field validation failures

400 malformed request      422 well-formed but semantically invalid
401 no valid identity       403 identity present, not authorized for this
404 doesn't exist            409 conflicts with current state
429 rate limited              500 unexpected server-side failure (never client's fault)

Validation: accumulate ALL field errors, don't fail-fast on the first
500 bodies: generic message only -- full detail goes to server-side logs
problem+json (RFC 7807): a standard shape worth knowing, not mandatory
```
