The difference between a usable error and a useless one is whether a program can
branch on it without reading English.
{
"type": "https://api.acme.com/errors/insufficient-funds",
"title": "Insufficient funds",
"status": 422,
"code": "INSUFFICIENT_FUNDS",
"detail": "Balance 4200 is below the requested 5000, in minor units.",
"request_id": "req_01J9X2K",
"errors": [
{ "field": "amount", "code": "EXCEEDS_BALANCE" }
]
}
The code is the contract. It is stable, it never gets reworded for clarity, and
it is what a client switches on — whereas title and detail exist for a human
reading a log and may change freely. Systems that ship only a message force every
consumer to string-match, and then a copy edit becomes a breaking change nobody
predicted.
The errors array is what makes a form usable, because it maps each problem to
the field that caused it. Returning the first validation failure only means the
user fixes one thing, resubmits, and discovers the next — an interaction that
takes five round trips where one would do.
request_id is the cheapest thing on the list and the most useful during an
incident. A user quoting it lets you find the exact trace, which turns "it failed
this morning" into a specific request in a specific service.
What is deliberately absent: stack traces, SQL fragments, internal hostnames and
identifiers. They are useless to the caller and are reconnaissance for anyone
probing the API — and they end up in the client's own logs, which are outside
your control entirely.