Why this matters
APIs fail. When they do, developers need clear, predictable errors to debug fast. Consistent error formats reduce support tickets, speed integration, and make monitoring easier.
- Real tasks you’ll face: design an error schema, map errors to HTTP status codes, provide validation error details, include trace IDs for observability, and document retry guidance.
- Goal: one stable error shape across all endpoints and services.
Who this is for
API engineers, backend developers, and technical writers who want to make APIs easier to integrate and support.
Prerequisites
- Basic HTTP knowledge (status codes, headers, content types)
- JSON fundamentals
- Familiarity with your API style (REST, GraphQL, or gRPC)
Concept explained simply
A consistent error format means every failure response follows the same JSON shape, no matter the endpoint or service. Clients always know where to find the machine-readable code, the human-readable message, and extra details.
Mental model
Think of your error as a reliable envelope:
- Outside: HTTP status indicates broad category (4xx client, 5xx server).
- Inside: a fixed JSON object with fields the client can always trust.
Suggested core fields (open for details)
- code (string, stable): machine-readable key like "VALIDATION_FAILED".
- message (string, human): concise description safe to show users.
- type (string): category or problem type (e.g., "validation_error").
- trace_id (string): helps support correlate logs.
- details (array/object): structured context (e.g., field errors).
- retry_after (number, optional): seconds until safe retry (e.g., rate limit).
- status (number, optional): duplicate of HTTP code inside body (useful in logs).
- documentation (string, optional): a stable identifier or URL to error docs.
Worked examples
1) Validation error (REST)
Status: 422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_FAILED",
"type": "validation_error",
"message": "One or more fields are invalid.",
"status": 422,
"trace_id": "af1b2c3d",
"details": [
{ "field": "email", "issue": "format", "message": "Must be a valid email." },
{ "field": "age", "issue": "min", "expected": 18, "actual": 15 }
]
}
}Notes: group multiple field errors in details; keep code stable across locales.
2) Auth error (REST)
Status: 401 Unauthorized (with WWW-Authenticate header as applicable)
{
"error": {
"code": "UNAUTHENTICATED",
"type": "auth_error",
"message": "Valid credentials are required.",
"status": 401,
"trace_id": "9d7e6f5a"
}
}Use 403 Forbidden when authenticated but not allowed.
3) Rate limit (REST)
Status: 429 Too Many Requests
{
"error": {
"code": "RATE_LIMITED",
"type": "throttle",
"message": "Rate limit exceeded. Please retry later.",
"status": 429,
"retry_after": 12,
"trace_id": "r1t2u3v4"
}
}Also include headers like Retry-After, X-RateLimit-Remaining if your policy supports them.
4) Server error (REST)
Status: 500 Internal Server Error
{
"error": {
"code": "INTERNAL_ERROR",
"type": "server_error",
"message": "An unexpected error occurred.",
"status": 500,
"trace_id": "e123abcd"
}
}Never expose stack traces or internal SQL; use trace_id for investigation.
5) GraphQL error (HTTP 200, errors array)
{
"data": null,
"errors": [
{
"message": "Not authenticated.",
"extensions": {
"code": "UNAUTHENTICATED",
"trace_id": "gql-7777",
"http": { "status": 401 }
}
}
]
}Keep extensions.code aligned with your REST error codes for consistency.
Design your error shape (5 steps)
- Pick a stable envelope: { "error": { code, message, type, details?, trace_id } }.
- Map HTTP statuses: 400/404/409/422/429 for client issues; 401/403 for auth; 5xx for server failures.
- Define codes: a closed list like VALIDATION_FAILED, UNAUTHENTICATED, NOT_FOUND, RATE_LIMITED, CONFLICT, INTERNAL_ERROR.
- Structure details: decide schemas for validation, quotas, and upstream failures.
- Observability: add trace_id and propagate X-Request-ID from incoming requests.
Anti-patterns to avoid
- Returning HTML error pages from JSON APIs.
- Inconsistent envelopes across endpoints.
- Using only messages without machine-readable codes.
- Leaking stack traces or secrets.
- Using 200 for errors in REST (reserve for GraphQL error arrays only).
Common mistakes and self-check
- Mistake: Different error shapes in different services. Self-check: Compare responses from 3 endpoints; envelopes identical?
- Mistake: Overloading 400 for everything. Self-check: Do you use 401/403/404/409/422/429 appropriately?
- Mistake: No stable code. Self-check: Can clients switch on error.code reliably?
- Mistake: No trace_id. Self-check: Can support correlate user reports to logs?
- Mistake: Localization in code. Self-check: Keep code stable; localize message only.
Exercises
Complete the exercise below. You can check your work with the solution. Tip: write your examples as if they shipped to production today.
Exercise 1: Design a consistent error schema for an Orders API
- Propose a single JSON error envelope for the API.
- Provide error examples for: missing auth, invalid input (multiple fields), rate limit, not found, and server crash.
- Include appropriate HTTP statuses and any helpful headers.
- Add a trace_id and a field-level details structure for validation.
What good output looks like
One error schema reused across all cases; stable codes; correct statuses; structured details; retry guidance for 429; no internal stack traces.
- One envelope used in all examples
- Codes are stable and documented
- Status codes match semantics
- trace_id included
- Validation details are structured
Practical projects
- Retrofit your existing API to emit the unified error envelope; add a gateway test that asserts shape, fields, and status codes.
- Add a middleware/filter that injects trace_id and maps thrown exceptions to standardized codes.
- Create a small SDK function parseApiError(response) returning { code, message, retriable, fields? } for client developers.
Learning path
- Start: define your error codes and envelope.
- Implement: add exception-to-error mapping in one service.
- Expand: roll out to all services and document in the API guide.
- Polish: add rate limit headers, localization of messages, and monitoring for top error codes.
Next steps
- Document your error codes in the API reference.
- Add contract tests to prevent schema drift.
- Work with support to align troubleshooting based on trace_id and codes.
Mini challenge
Pick one real endpoint and instrument it so every error includes a trace_id, consistent code, and appropriate status. Verify with curl or your API client.
Quick Test
The Quick Test is available to everyone; only logged-in users get saved progress.