Errors Are API Surface
Consumers build against your failure modes as much as your successes. Inconsistent errors (sometimes a string, sometimes an object, sometimes HTML) force every client into defensive parsing — and turn debugging into archaeology.
every error response answers FOUR questions:
WHAT happened (stable machine code)
WHY (human message)
WHERE (which field/param)
WHAT NEXT (retry? fix request? contact support?)
A Consistent Envelope
{
"error": {
"code": "card_declined",
"message": "Your card was declined by the issuer.",
"field": null,
"request_id": "req_9f2ka83",
"doc_url": "https://api.ride.example/errors#card_declined",
"retryable": false
}
}
| Element | Purpose |
|---|---|
code | Stable snake_case string — clients branch on this, never on messages |
message | Human-readable, safe to display, no internals |
field | Points at offending input for validation errors |
request_id | Correlates client report ↔ server logs — debugging lifeline |
retryable | Encodes the retry decision so clients don’t guess |
Machine Codes vs Messages
CODES are contract: "insufficient_balance" forever means that
MESSAGES are prose: reword freely, localize, A/B test
branching on message text = broken clients at next copy edit.
rule: codes for logic, messages for humans, never swapped
The Retryability Axis
The most consequential field is retryable:
retryable: true → transient: 503s, lock timeouts, upstream blips
client SHOULD retry with backoff
retryable: false → deterministic: validation, authz, insufficient funds
retrying is waste; user must act
encoding this explicitly beats clients inferring from status class,
because some 4xx ARE transient (429 with Retry-After) and
some 5xx are NOT (unrecoverable internal assertion)
Validation Errors
{
"error": {
"code": "validation_failed",
"message": "Request has invalid fields.",
"fields": [
{"field": "pickup.lat", "code": "out_of_range", "message": "Latitude must be -90..90"},
{"field": "scheduled_at", "code": "in_past", "message": "Cannot schedule in the past"}
]
}
}
Return all field violations per request, not the first — one round-trip per bug instead of N.
Anti-Patterns
✗ 200 OK { "success": false } breaks caches, monitors, retries
✗ leaking stack traces / SQL security + professionalism fail
✗ generic "Something went wrong" no correlation, no action possible
✗ new error shape per endpoint client-side combinatorial explosion
✗ HTTP 500 for user input errors poisons alerting AND retry logic
That last one cuts both ways operationally: pages fire on 5xx spikes; if validation noise rides in 5xx, real outages hide in the pager.
Interview Framing
Error modeling rarely gets its own question but scores constantly: any design answer improves when you sketch the error envelope once and reference it (“all endpoints return this shape; 5xx carries retryable=true”). The request_id mention specifically signals production experience — it’s the field everyone wishes they’d had.
Premium Content
Unlock Error Modeling and all premium lessons with a subscription.
From ₹199.99/year — See plans