API · Rest

REST API Error Handling — A Field Guide

How to design REST API error responses that are both machine-parseable and useful for debugging, using RFC 9457 Problem Details as the shape.

John Kihiu12 min read

A REST API's error responses get exercised more than almost any other part of the contract, because clients hit them constantly during integration and intermittently forever after in production — and yet error handling is usually the last thing designed, often inconsistent across endpoints because each one was built by whoever got to it first. A consistent error shape, applied everywhere, is one of the cheapest things you can do to make an API pleasant to integrate against.

RFC 9457 Problem Details as the shape

RFC 9457 (which obsoletes the earlier RFC 7807) standardizes a JSON error shape so clients don't need bespoke parsing per API: type (a URI identifying the error kind), title (short human-readable summary), status (the HTTP status code, redundant with the response line but useful when the body is logged separately), detail (specifics for this occurrence), and instance (a URI identifying this specific occurrence, useful for support tickets). It's a genuinely good default to adopt wholesale rather than inventing your own error envelope — the format is content-type-negotiable (application/problem+json) and every field has a clear, narrow purpose.

HTTP · RFC 9457 PROBLEM DETAILS
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/invalid-invoice-total",
  "title": "Invoice total must be positive",
  "status": 422,
  "detail": "Field 'total_cents' was -500; totals cannot be negative.",
  "instance": "/v1/invoices/4471",
  "errors": [{"field": "total_cents", "message": "must be >= 0"}]
}

The status code is the first signal, the body is the second

A client's retry logic and error-handling branches should be driven primarily by the status code, not by parsing title or detail strings — those are for humans reading logs, not for programmatic branching. That means the status code has to be chosen correctly and consistently: 409 Conflict for a version mismatch or duplicate resource, 422 for semantic validation failure, 429 for rate limiting, 503 for a dependency being down. A machine-readable type URI in the body is the right place for finer-grained branching than the status code alone provides — a client can switch on type without ever parsing English text.

Validation errors need field-level detail, not one string

A form or API client integrating against your API needs to know which field failed and why, not just that "validation failed" — a single opaque error message forces the client to guess or, worse, to re-validate the entire payload client-side using undocumented rules mirrored from your server. The errors array pattern (field, message, optionally a machine-readable code per field) shown above isn't part of RFC 9457 itself but is a common, sensible extension for exactly this case.

Log the instance URI, not just the error

Generating a unique instance value per error occurrence (a UUID, or a URI containing one) and logging it server-side at the same log level as the underlying exception turns "a customer says they got an error" into a five-second log lookup, instead of a guessing game based on approximate timestamps.

Don't leak internals in error messages

A stack trace, a SQL error message, or an internal file path in a production error response is an information disclosure bug, not a debugging convenience — it tells an attacker your database engine, your ORM, sometimes your table names. Catch and translate internal exceptions into the Problem Details shape at the boundary, and keep the actual stack trace in your server-side logs, correlated by the instance identifier, where only your team can see it.

Don't return 200 with an error in the body

A response that returns HTTP 200 with {"success": false, "error": "..."} in the body defeats every HTTP-aware client, proxy, and monitoring tool that keys off status codes — retries won't trigger, alerting won't fire, and generic HTTP clients will report success. Use the status code; that's what it's for.

Wrapping up

A consistent error contract — RFC 9457 Problem Details or an equivalent, correct status codes driving client branching, field-level detail for validation failures, and internals kept out of the response body — costs little to build and pays for itself the first time an integrator debugs a production issue without opening a support ticket.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.