Error Handling
Expected vs unexpected failures, and returning something the caller can actually act on.
35 minDifficulty 3/5api · reliabilityAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.
Before this
Why this exists
A client sends a malformed request and the server responds with a generic 500 and a stack trace dumped straight into the JSON body — the caller learns nothing actionable, and now anyone watching the response also knows internal file paths and library versions. A well-designed error response tells the caller exactly what went wrong and what they can do about it, and tells an attacker nothing extra.
The mental model
Split every failure into two categories immediately: EXPECTED failures (bad input, a resource not found, insufficient permission) that are a normal part of the API's contract and deserve a specific, informative response; and UNEXPECTED failures (a bug, a database outage) that represent something genuinely broken and should be logged loudly while showing the caller only a generic, safe message.
How it works
The HTTP status code is the first, coarsest signal
400 means the client sent something wrong (fixable by changing the request); 401 means authentication is missing or invalid; 403 means authenticated but not permitted; 404 means the resource doesn't exist; 500 means the server itself failed. Returning 200 with an `{ error: ... }` body for a genuine failure discards this signal entirely, forcing every caller to parse the body just to know if the request even succeeded.
An error body should be structured and machine-parseable, not just a string
`{ error: { code: 'invalid_email', message: 'Enter a valid email address', field: 'email' } }` lets a client programmatically highlight the specific field and even show a translated message keyed on `code`, whereas `{ error: 'Enter a valid email address' }` alone forces the client to string-match the exact English text to do anything beyond displaying it verbatim.
Never leak internals into a response the client will see
A stack trace, a database error message, or an internal file path in an error response is both a security risk (revealing implementation details useful to an attacker) and useless noise to a legitimate caller. Log the full detail server-side, where a developer can actually use it, and send the client only what THEY need: a code, a safe message, and maybe a request ID to reference when reporting the issue.
A single, centralized error handler keeps responses consistent
Scattering try/catch blocks with ad-hoc response formatting across forty different route handlers produces forty subtly different error response shapes. A single error-handling middleware (or equivalent), which every route's errors flow through, guarantees one consistent structure, one place to add a new field (like a request ID) to every error response, and one place to decide what's safe to expose.
The mechanism
A route handler either succeeds normally or throws/passes an error to a centralized handler. That handler inspects the error's type — a known, expected error (like a validation failure, carrying a specific code and safe message) versus an unknown, unexpected one (a raw exception with no such structure). Known errors are mapped to their appropriate status code and structured body; unknown errors are logged in full detail server-side and returned to the client as a generic 500 with no internal information exposed.
What people get wrong
- Always returning HTTP 200 and putting error details in the response body is simpler and more consistent for clients.
- This discards the status code's built-in, universally-understood signal, forcing every client to parse the body just to know if the request succeeded, and breaks tooling (like HTTP caching, monitoring, and retry logic) that relies on status codes to distinguish success from failure. This pattern, sometimes justified as 'simpler', actually adds work for every caller and loses interoperability with the broader ecosystem of tools that understand standard HTTP status codes.
- Showing a detailed error message, including technical specifics, is more helpful to the client than a generic one.
- Detail is helpful for EXPECTED errors the client can act on (which field was invalid, and why) but actively harmful for UNEXPECTED errors, where exposing internals (stack traces, database schema hints) helps an attacker more than it helps a legitimate caller who can't fix a server bug anyway. The right amount of detail depends entirely on whether the error is something the CALLER caused and can fix, versus something the SERVER is responsible for — conflating the two leads to either unhelpful vagueness or dangerous oversharing.
- A try/catch around the whole route handler, logging the error and returning a generic message, is sufficient error handling.
- This treats every error identically, losing the distinction between an expected failure (which deserves a specific status code and message the client can act on) and a genuine bug (which deserves loud internal logging and a generic client-facing response) — both end up looking the same to the caller. Without that distinction, a client gets an unhelpful generic 500 even for something as simple and actionable as 'that email is already registered', which should have been a clear 409 with a specific message.
When not to use it
- The error is a genuine, unrecoverable bug — a null pointer exception, a failed database connection.
- Log the full detail (stack trace, context) server-side for debugging, and return a generic 500 with no internal detail to the client — this is exactly the 'unexpected error' case.
- The API is public-facing and used by third-party developers who need to build reliable integrations.
- A well-documented, stable set of error codes (not just messages, which might change wording) that third-party code can reliably branch on programmatically, since message text isn't a contract they should depend on.
Terms
- Expected error
- — A failure that's a normal, anticipated part of an API's behaviour — invalid input, a missing resource — deserving a specific status code and actionable message.
- Unexpected error
- — A genuine bug or infrastructure failure not anticipated by the API's design, which should be logged in full detail and returned to the client as a generic, safe response.
- Error code
- — A stable, machine-readable identifier (distinct from the human-readable message) that client code can reliably branch on without depending on exact message wording.
- Centralized error handler
- — A single piece of code every route's errors flow through, ensuring consistent response structure and a single place to decide what detail is safe to expose.
In an interview
Why is it a security risk to include a raw exception's stack trace or message in an API's error response?
- a stack trace can reveal internal file paths, library versions, and code structure useful for finding other vulnerabilities
- a database error message can reveal schema details, like table or column names
- the fix is logging full detail server-side, returning only a generic, safe message (plus a reference id) to the client
Can you recall it?
Why should an API distinguish between 'expected' and 'unexpected' errors when deciding what to send back to the client?