Loading content...
Visit nearly any API documentation, and you'll find exhaustive coverage of success paths: endpoints, parameters, request/response schemas, authentication flows. Then look for error documentation. What you'll often find is... almost nothing. Perhaps a generic "Errors" section listing 400, 401, and 500 without context. Maybe a mention that "errors follow a standard format."
This documentation gap creates real problems. Developers discover error conditions through production incidents. Integration partners build brittle error handling because they don't know what to expect. Support tickets multiply because consumers can't self-diagnose failures.
Comprehensive error documentation is a competitive advantage. It signals maturity, builds trust, and reduces integration friction—yet it remains one of the most neglected aspects of API design.
By the end of this page, you will understand what error documentation should include, strategies for organizing error catalogs, techniques for keeping documentation current, and tools and formats that make error documentation discoverable and useful.
Error documentation directly impacts the success of API consumers and the cost of supporting your API. The investment pays dividends across multiple dimensions:
Each undocumented error generates multiple support interactions: the initial discovery, attempts to reproduce, escalation for investigation, communication of the resolution. A single paragraph of documentation can eliminate hundreds of hours of support over the API's lifetime.
Effective error documentation answers the questions developers ask when encountering an error. For each error code or condition, include:
| Component | Purpose | Example |
|---|---|---|
| Error Code | Unique identifier for programmatic handling | PAYMENT.CARD_DECLINED |
| HTTP Status | HTTP status code returned | 402 Payment Required |
| Summary | One-line description of the error | Payment method was declined by issuer |
| Description | Detailed explanation of why this occurs | This error occurs when the card issuer refuses the charge. Common reasons include... |
| Causes | Specific conditions that trigger this error | Insufficient funds, Card expired, Fraud detection triggered |
| Resolution Steps | How to fix or work around the error |
|
| Retryable? | Whether retrying might succeed | No - customer action required |
| Example Response | Actual JSON of the error response | Complete response body example |
| Related Errors | Similar errors consumers might confuse | See also: PAYMENT.CARD_EXPIRED, PAYMENT.INVALID_CARD |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
# PAYMENT.CARD_DECLINED **HTTP Status:** 402 Payment Required ## SummaryThe payment method was declined by the card issuer. ## DescriptionThis error occurs when the customer's card issuer refuses to authorize the transaction. The issuer does not provide detailed reasons to merchants for security reasons, but common causes include insufficient funds, suspected fraud, or card restrictions. ## Common Causes | Cause | Description ||-------|-------------|| Insufficient funds | Card/account lacks funds for this amount || Fraud prevention | Issuer's fraud detection blocked the transaction || Card restrictions | Card may not be authorized for this merchant type || Daily limit | Customer exceeded their daily spending limit | ## Resolution Steps 1. **Try a different payment method** — Use a different card or payment method2. **Contact the card issuer** — The customer should call their bank for details3. **Verify billing details** — Ensure address matches card registration4. **Try a smaller amount** — May help identify if it's a limit issue ## Retryable?**No** — Retrying with the same card will produce the same result. Customer action is required before retry. ## Example Response ```json{ "code": "PAYMENT.CARD_DECLINED", "message": "The card ending in 4242 was declined", "details": { "declineCode": "insufficient_funds", "cardLast4": "4242", "cardBrand": "visa" }, "requestId": "req_abc123", "timestamp": "2024-01-15T14:30:00Z"}``` ## Related Errors- [PAYMENT.CARD_EXPIRED](/errors/payment-card-expired) — Card past expiration- [PAYMENT.INVALID_CVV](/errors/payment-invalid-cvv) — CVV verification failed- [PAYMENT.INSUFFICIENT_FUNDS](/errors/payment-insufficient-funds) — Specific decline code when issuer provides itAs your API grows, errors accumulate. Thoughtful organization makes documentation navigable and useful.
Recommended Approach: Multiple Views
The best API documentation provides multiple entry points to error information:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
# Per-endpoint error documentation in OpenAPIpaths: /orders: post: summary: Create an order operationId: createOrder responses: '201': description: Order created successfully content: application/json: schema: $ref: '#/components/schemas/Order' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: invalid_json: summary: Malformed request body value: code: "REQUEST.INVALID_JSON" message: "Request body is not valid JSON" '401': $ref: '#/components/responses/Unauthorized' '422': description: Validation failed content: application/json: schema: $ref: '#/components/schemas/ValidationErrorResponse' examples: missing_items: summary: Order has no items value: code: "VALIDATION.ORDER_EMPTY" message: "Order must contain at least one item" details: field: "items" invalid_quantity: summary: Invalid item quantity value: code: "VALIDATION.INVALID_QUANTITY" message: "Item quantity must be between 1 and 100" details: field: "items[0].quantity" value: 0 constraint: min: 1 max: 100 # Reusable error responsescomponents: responses: Unauthorized: description: Authentication required or failed content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: missing_token: value: code: "AUTH.TOKEN_MISSING" message: "Authorization header required" invalid_token: value: code: "AUTH.TOKEN_INVALID" message: "Authorization token is invalid"Beyond standalone error pages, errors should be documented where developers encounter them—in endpoint documentation, SDK references, and even in the errors themselves.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
/** * Creates a new order in the system. * * @param items - Array of items to include in the order * @param shippingAddress - Destination address for delivery * @returns The created order with generated ID and status * * @throws {ValidationError} When items are empty or invalid * - `VALIDATION.ORDER_EMPTY`: No items provided * - `VALIDATION.INVALID_QUANTITY`: Quantity out of range (1-100) * - `VALIDATION.INVALID_SKU`: Unknown product SKU * * @throws {AuthenticationError} When authentication fails * - `AUTH.TOKEN_EXPIRED`: Refresh your token and retry * - `AUTH.TOKEN_INVALID`: Re-authenticate required * * @throws {PaymentError} When payment processing fails * - `PAYMENT.CARD_DECLINED`: Customer must use different card * - `PAYMENT.INSUFFICIENT_FUNDS`: Customer's card lacks funds * * @throws {InventoryError} When items are unavailable * - `INVENTORY.OUT_OF_STOCK`: Reduce quantity or remove item * - `INVENTORY.DISCONTINUED`: Item no longer available * * @example * // Handling errors appropriately * try { * const order = await client.orders.create({ * items: [{ sku: 'WIDGET-001', quantity: 2 }], * shippingAddress: address * }); * console.log('Order created:', order.id); * } catch (err) { * if (err instanceof ValidationError) { * // Show validation messages to user * displayErrors(err.details.errors); * } else if (err instanceof PaymentError) { * // Prompt for different payment method * showPaymentForm({ error: err.message }); * } else if (err instanceof InventoryError) { * // Update cart to reflect availability * refreshCart(); * } else { * // Unexpected error - log and show generic message * console.error('Order failed:', err); * showGenericError(); * } * } */async function createOrder( items: OrderItem[], shippingAddress: Address): Promise<Order>;Outdated documentation is often worse than no documentation—it actively misleads consumers. Keeping error documentation current requires systematic processes:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
// Errors defined as code with full documentation metadata// This single source generates API responses AND documentation export const Errors = { AUTH_TOKEN_EXPIRED: defineError({ code: "AUTH.TOKEN_EXPIRED", httpStatus: 401, // Documentation fields summary: "Authentication token has expired", description: ` Access tokens expire after a configurable period (default: 1 hour). When expired, requests fail with this error until a new token is obtained. `, causes: [ "Token was issued more than TTL ago", "Token was explicitly revoked" ], resolution: [ "Use refresh token to obtain new access token", "Re-authenticate if refresh token also expired" ], retryable: true, retryAfter: "After obtaining new token", // Links docs: "/errors/auth-token-expired", guide: "/guides/authentication#token-refresh", // Version tracking since: "1.0.0", deprecated: false, // Example response example: { expiredAt: "2024-01-15T14:00:00Z" }, // Related errors related: ["AUTH.TOKEN_INVALID", "AUTH.TOKEN_MISSING"] }), // ... more errors}; // Generate markdown docsfunction generateDocs(): string { return Object.values(Errors) .map(error => `## ${error.code} **HTTP Status:** ${error.httpStatus} ### Summary${error.summary} ### Description${error.description} ### Causes${error.causes.map(c => `- ${c}`).join('\n')} ### Resolution${error.resolution.map((r, i) => `${i + 1}. ${r}`).join('\n')} ### Retryable${error.retryable ? `Yes — ${error.retryAfter}` : 'No'} `).join('\n---\n');}Treat documentation completeness as a testable property. Just as you test that endpoints return correct responses, test that every returned error code has corresponding documentation. Automation catches gaps before users do.
Different tools and formats serve different documentation needs. Choose based on your existing toolchain and consumer preferences.
| Format/Tool | Best For | Considerations |
|---|---|---|
| OpenAPI/Swagger | REST APIs with generated docs | Standard format; broad tooling; can be verbose for complex errors |
| Markdown/Static Sites | Detailed, prose-heavy documentation | Flexible layout; good for guides and examples; manual maintenance |
| API Reference Platforms | Unified API documentation | ReadMe, Stoplight, Redocly; search and interactivity built-in |
| Error Lookup Endpoint | Runtime error discovery | GET /errors/{code} returns full docs; self-service debugging |
| SDK Documentation | Language-specific integration | Javadoc, TSDoc, etc.; exception classes fully documented |
| Status Pages | System-wide error status | For operational issues affecting many requests; not for per-request errors |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
// Error lookup API endpoint - self-service error documentationimport express from 'express';import { errorCatalog, ErrorDocumentation } from './docs/error-catalog'; const app = express(); // Initialize error indexconst errorIndex = new Map<string, ErrorDocumentation>( errorCatalog.map(e => [e.code, e])); // GET /errors - List all error codesapp.get('/errors', (req, res) => { const { category, httpStatus, search } = req.query; let results = [...errorCatalog]; if (category) { results = results.filter(e => e.category === category); } if (httpStatus) { results = results.filter(e => e.httpStatus === parseInt(httpStatus as string)); } if (search) { const term = (search as string).toLowerCase(); results = results.filter(e => e.code.toLowerCase().includes(term) || e.summary.toLowerCase().includes(term) || e.description.toLowerCase().includes(term) ); } res.json({ errors: results.map(e => ({ code: e.code, httpStatus: e.httpStatus, summary: e.summary, category: e.category, detailUrl: `/errors/${encodeURIComponent(e.code)}` })), categories: [...new Set(errorCatalog.map(e => e.category))], total: results.length });}); // GET /errors/:code - Full documentation for specific errorapp.get('/errors/:code', (req, res) => { const code = decodeURIComponent(req.params.code); const error = errorIndex.get(code); if (!error) { return res.status(404).json({ code: 'ERROR.NOT_FOUND', message: `No documentation found for error code: ${code}`, suggestion: 'Use GET /errors to list all known error codes' }); } res.json({ ...error, links: { self: `/errors/${encodeURIComponent(code)}`, related: error.relatedErrors.map( r => `/errors/${encodeURIComponent(r)}` ), humanReadable: `https://docs.example.com/errors/${error.code}` } });}); // Include error lookup in error responses themselvesfunction formatErrorResponse(error: ApiError, requestId: string) { return { code: error.code, message: error.message, details: error.details, requestId, timestamp: new Date().toISOString(), documentation: { url: `https://api.example.com/errors/${encodeURIComponent(error.code)}`, humanUrl: `https://docs.example.com/errors/${error.code}` } };}Errors evolve over time. New errors are introduced, old errors are deprecated, and error details change. Tracking these changes helps consumers maintain compatibility.
details structure changes, note what was added/removed.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
# Error Changelog ## Version 2.3.0 (2024-01-15) ### New Errors - **PAYMENT.3DS_REQUIRED** — Returned when payment requires 3D Secure authentication. See [3D Secure Guide](/guides/3d-secure) for handling. - **USER.MFA_REQUIRED** — Returned when action requires MFA verification. Includes `mfaMethods` in details with available verification options. ### Deprecated Errors - **AUTH.SESSION_EXPIRED** — *Deprecated*. Will be removed in v3.0. Use `AUTH.TOKEN_EXPIRED` instead. Both are returned during transition period. ### Changed - **VALIDATION.FIELD_INVALID** — Now includes `suggestedValue` in details when the system can infer a correction (e.g., typo in country code). --- ## Version 2.2.0 (2023-11-01) ### New Errors - **RATE_LIMIT.QUOTA_EXHAUSTED** — Separate from `RATE_LIMIT.EXCEEDED`. Returned when monthly quota is exhausted (not rate-limited). Includes `quotaResetAt` timestamp. ### Breaking Changes - **VALIDATION.REQUIRED_FIELD** renamed to **VALIDATION.FIELD_REQUIRED** for consistency. Old code will not work. --- ## Migration Guide: v2.1 to v2.2 ### Error Code Rename If your code handles `VALIDATION.REQUIRED_FIELD`, update to `VALIDATION.FIELD_REQUIRED`: ```javascript// Before (v2.1)if (error.code === 'VALIDATION.REQUIRED_FIELD') { ... } // After (v2.2+)if (error.code === 'VALIDATION.FIELD_REQUIRED') { ... }```Never rename an error code in a minor or patch version. Consumers programmatically match on codes. A rename is a breaking change requiring a major version bump and migration guidance.
Error documentation transforms your API from frustrating to professional. Let's consolidate the key principles:
Module Complete:
You've completed the Error Handling in APIs module. You understand when to use exceptions versus return codes, the nuances of checked versus unchecked exceptions, how to design informative error responses, and how to document errors comprehensively.
These skills elevate your APIs from functional to professional. Well-handled and well-documented errors separate APIs that developers love from APIs that developers tolerate.
Congratulations! You've mastered error handling in API design. From deciding between exceptions and return codes, through crafting informative error responses, to documenting every failure mode—you now have the tools to build APIs that fail gracefully and communicate failures effectively.