How should an API return errors — what does a good standardized error response look like (e.g. RFC 7807 Problem Details)?
An error response is still part of your API's contract — 'it broke, here's a stack trace' is not a design, and RFC 7807 gives every client the same shape to parse.
LEARN BY DOINGPut your intuition to work3 steps
How should an API return errors — what does a good standardized error response look like (e.g. RFC 7807 Problem Details)?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Why ad-hoc error bodies hurt
Every backend engineer has worked with an API where errors are an afterthought: one endpoint returns {"error": "not found"}, another returns {"message": "Invalid input", "err": true}, a third returns a 200 OK with a success-shaped envelope wrapping {"success": false} inside it, and a fourth, when something truly breaks, lets an unhandled exception's stack trace leak straight into the response body. A client integrating against that API can't write one error-handling code path — it has to special-case every endpoint, and it has no stable field to branch on programmatically.
A good error response has to satisfy several audiences at once: it needs a correct HTTP status code so that generic infrastructure (retries, circuit breakers, monitoring) behaves sensibly without reading the body at all; it needs a stable, machine-parseable identifier so client code can branch on what kind of error this is, not just present a status code; it needs a human-readable message for logs, support tickets, and developers debugging an integration; and it must never leak internals — no stack traces, no raw SQL, no internal file paths — in a production response.
#RFC 7807: Problem Details for HTTP APIs
RFC 7807 (soon superseded in numbering by RFC 9457, same shape) standardizes a JSON body for exactly this, served with the media type application/problem+json so a client can distinguish a structured error from an ordinary JSON success body just by looking at Content-Type. The shape has five reserved members, and you're free to add your own:
type— a URI identifying the category of problem. It doesn't have to resolve to anything real, but it should be stable and, ideally, dereferenceable to human-readable documentation about that error class. Defaults toabout:blankif omitted, which just means "see the status code."title— a short, human-readable summary of the problem type, meant to be the same across every occurrence of that type (don't put per-request detail here).status— the HTTP status code, duplicated into the body purely for convenience (logging pipelines and clients that inspect the body without also carrying the status code around).detail— a human-readable explanation specific to this occurrence, e.g. which field or which resource.instance— a URI identifying this specific occurrence, commonly just the request path, useful for correlating with logs.
Anything beyond those five is an "extension member" — just additional JSON keys sitting alongside the standard ones, and this is where you put whatever your specific error actually needs.
{"type": "https://api.example.com/errors/insufficient-funds","title": "Insufficient Funds","status": 422,"detail": "Account 4f3a-99c1 has a balance of 12.50, less than the requested transfer of 50.00.","instance": "/transfers/7a4e1c3d","balance": 12.50,"requested": 50.00}
Validation errors are the most common case that needs an extension member, since one request can fail for several reasons at once:
{"type": "https://api.example.com/errors/validation-failed","title": "Validation Failed","status": 400,"detail": "One or more fields failed validation.","instance": "/users","errors": [{ "field": "email", "message": "must be a valid email address" },{ "field": "age", "message": "must be a positive integer" }]}
#Wiring it up
A single error class plus one centralized error-handling middleware is usually enough to make this consistent across an entire API surface, so no individual route handler has to remember the envelope shape:
class ApiError extends Error {constructor({ status, title, detail, type, ...extensions }) {super(detail || title);this.status = status;this.title = title;this.detail = detail;this.type = type || 'about:blank';this.extensions = extensions;}}function errorHandler(err, req, res, next) {const status = err.status || 500;res.status(status).type('application/problem+json').json({type: err.type,title: err.title || 'Internal Server Error',status,detail: status >= 500 ? undefined : err.detail, // never leak internals on 5xxinstance: req.originalUrl,...err.extensions,});}module.exports = { ApiError, errorHandler };
#Picking the right status code
The body format only helps if the status code above it is actually correct — a client that only has time to check one thing will check the status code first.
| Status | When to use it |
|---|---|
400 Bad Request | Malformed syntax the server can't parse at all (broken JSON, wrong content type) |
401 Unauthorized | Missing or invalid credentials — the request isn't even authenticated |
403 Forbidden | Authenticated, but not allowed to do this |
404 Not Found | The resource doesn't exist (or you're intentionally hiding that it does) |
409 Conflict | The request conflicts with current state (duplicate resource, version mismatch) |
422 Unprocessable Entity | Syntactically valid but semantically invalid (business-rule violation) |
429 Too Many Requests | Rate limited — pair with a Retry-After header |
500 Internal Server Error | Something broke that the client couldn't have prevented |
503 Service Unavailable | Temporarily down/overloaded — pair with Retry-After |
🧠 Check your understanding
0/3 · 0/3 answered1. In RFC 7807 Problem Details, what is the 'type' field for?
2. Why should a validation-failure response include a 'status' field inside the JSON body, when the HTTP status line already carries it?
3. What is the main danger of putting raw exception messages into an error response's detail field on a production 500 error?