Errors, and how to read them
Write the error reader once. Every non-2xx on every surface of the platform is the same object, so a client needs one function to read a refusal, and it works against endpoints nobody has written yet.
Code
Three parts, three audiences. status is the canonical code and the only part to
branch on — it is stable, while the HTTP status alone cannot tell an ALREADY_EXISTS
from a lost race. message is written for the person reading the log, in English, and
never carries internal detail; do not parse it and do not show it to an end user.
details is where a form finds what to underline: a BAD_REQUEST entry lists field
violations by their path in the request body you sent, and a REQUEST_INFO entry carries
the reference id to quote when you report a 500.
Two codes surprise people, and both are deliberate. A resource you may not see is
NOT_FOUND, never PERMISSION_DENIED — the alternative would confirm that it exists. And
a 409 is two different things: ALREADY_EXISTS if the id or unique key is taken,
ABORTED if somebody wrote first and your If-Match is stale.
13. Errors
One shape, for every non-2xx, on every surface:
Code
codeis the HTTP status,statusis the canonical code, and they MUST agree with the table below.messageis for a developer reading a log — English, no trailing period required, and never raw internal detail: an exceptionwhat(), a repository error, a mail-send failure or a storage path never reaches a client.detailsis an array of typed objects,typedrawn from a closed set (D6):BAD_REQUEST(withfield_violations),PRECONDITION_FAILURE,QUOTA_FAILURE,RESOURCE_INFO,RETRY_INFO,REQUEST_INFO,LOCALIZED_MESSAGE. Adding a type is a standard change, not a local one.field_violations[].fieldis a field path in the request body (items[2].url_key), so a form can attach the message to an input without parsing prose.
| Canonical status | HTTP | Use for |
|---|---|---|
INVALID_ARGUMENT | 400 | Malformed request, bad filter, unknown sortable field |
FAILED_PRECONDITION | 400 | Request valid, system state forbids it |
OUT_OF_RANGE | 400 | skip past the cap, page_size negative |
UNAUTHENTICATED | 401 | Missing/expired/invalid token |
PERMISSION_DENIED | 403 | Authenticated, not allowed — and existence is already known to the caller |
NOT_FOUND | 404 | Absent, or present but invisible to this caller |
ALREADY_EXISTS | 409 | Create with a taken id or unique key |
ABORTED | 409 | etag mismatch (§14) |
RESOURCE_EXHAUSTED | 429 | Rate limit, quota. MUST carry RETRY_INFO |
CANCELLED | 499 | Client disconnected |
INTERNAL | 500 | Bug. MUST carry REQUEST_INFO |
NOT_IMPLEMENTED | 501 | Declared but unbuilt |
UNAVAILABLE | 503 | Dependency down, shedding load |
DEADLINE_EXCEEDED | 504 | Upstream timeout |