REST API Error Handling Standards: RFC 7807 & Beyond
Design consistent, machine-readable error responses using RFC 7807 Problem Details, custom error codes, and structured error envelopes that clients can actually parse.
What you'll learn
- ✓What RFC 7807 Problem Details is and how to implement it
- ✓How to design application-specific error codes
- ✓Patterns for validation errors with field-level detail
- ✓How to structure error envelopes consistently
- ✓Common error response anti-patterns to avoid
Prerequisites
- •Basic REST API knowledge
- •Understanding of HTTP status codes
Every API returns errors. The question is whether your errors are useful or whether clients have to guess what went wrong by parsing a string. This article covers the RFC 7807 standard, error code design, and practical patterns for consistent error responses.
The problem with ad-hoc errors
Most APIs start with errors like this:
{ "error": "Something went wrong" }
Or worse, different endpoints return different shapes:
// Endpoint A
{ "error": "Not found" }
// Endpoint B
{ "message": "User not found", "code": 404 }
// Endpoint C
{ "errors": ["Invalid email", "Name required"] }
Clients cannot reliably parse these. Every integration becomes a special case.
RFC 7807: Problem Details for HTTP APIs
RFC 7807 (updated by RFC 9457) defines a standard format for error responses. The content type is application/problem+json.
Required and optional fields
| Field | Required | Description |
|---|---|---|
type | Yes | A URI that identifies the error type. Serves as a stable identifier. |
title | Yes | A short, human-readable summary. Should not change between occurrences. |
status | Yes | The HTTP status code (repeated in the body for convenience). |
detail | No | A human-readable explanation specific to this occurrence. |
instance | No | A URI identifying this specific occurrence (e.g., a log correlation ID). |
Basic example
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/resource-not-found",
"title": "Resource not found",
"status": 404,
"detail": "No user with ID 'usr_abc123' exists.",
"instance": "/logs/errors/7f8a9b2c"
}
The type field matters
The type URI should be:
- Stable — never changes once published
- Dereferenceable — ideally points to documentation explaining the error
- Specific —
resource-not-foundis better thannot-found
// Good type URIs
"https://api.example.com/errors/insufficient-funds"
"https://api.example.com/errors/duplicate-email"
"https://api.example.com/errors/rate-limited"
// Bad type URIs
"https://api.example.com/errors/error"
"about:blank" // RFC allows this for generic HTTP errors, but it's not useful
Application-specific error codes
RFC 7807 is extensible — you can add custom fields. A numeric or string error code helps clients handle errors programmatically:
{
"type": "https://api.example.com/errors/payment-failed",
"title": "Payment failed",
"status": 422,
"detail": "The card was declined by the issuing bank.",
"code": "PAYMENT_CARD_DECLINED",
"decline_code": "insufficient_funds"
}
Designing an error code system
Define a catalog of error codes that is separate from HTTP status codes:
const ERROR_CODES = {
// Authentication errors (1xxx)
AUTH_TOKEN_EXPIRED: { status: 401, title: 'Token expired' },
AUTH_TOKEN_INVALID: { status: 401, title: 'Invalid token' },
AUTH_INSUFFICIENT_SCOPE:{ status: 403, title: 'Insufficient scope' },
// Validation errors (2xxx)
VALIDATION_FAILED: { status: 422, title: 'Validation failed' },
INVALID_EMAIL_FORMAT: { status: 422, title: 'Invalid email format' },
// Resource errors (3xxx)
RESOURCE_NOT_FOUND: { status: 404, title: 'Resource not found' },
RESOURCE_CONFLICT: { status: 409, title: 'Resource conflict' },
RESOURCE_GONE: { status: 410, title: 'Resource permanently removed' },
// Rate limiting (4xxx)
RATE_LIMITED: { status: 429, title: 'Rate limit exceeded' },
// Server errors (5xxx)
INTERNAL_ERROR: { status: 500, title: 'Internal server error' },
SERVICE_UNAVAILABLE: { status: 503, title: 'Service unavailable' },
};
Validation errors with field-level detail
Validation errors deserve special attention. Clients need to know which fields failed and why, so they can display inline errors in forms.
RFC 7807 does not define a fields or errors array, but extending the format is explicitly allowed:
{
"type": "https://api.example.com/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "The request body contains 3 validation errors.",
"errors": [
{
"field": "email",
"message": "Must be a valid email address.",
"code": "INVALID_FORMAT",
"value": "not-an-email"
},
{
"field": "age",
"message": "Must be at least 18.",
"code": "OUT_OF_RANGE",
"value": 12
},
{
"field": "username",
"message": "Already taken.",
"code": "DUPLICATE_VALUE",
"value": "johndoe"
}
]
}
Implementation: Express.js error handler
Here is a complete error handling setup:
// errors.js — custom error classes
export class AppError extends Error {
constructor(code, detail, extras = {}) {
super(detail);
this.code = code;
this.detail = detail;
this.extras = extras;
}
}
export class ValidationError extends AppError {
constructor(errors) {
super('VALIDATION_FAILED', `The request body contains ${errors.length} validation error(s).`);
this.fieldErrors = errors;
}
}
export class NotFoundError extends AppError {
constructor(resource, id) {
super('RESOURCE_NOT_FOUND', `No ${resource} with ID '${id}' exists.`);
}
}
// error-handler.js — Express middleware
import { ERROR_CODES } from './error-codes.js';
export function errorHandler(err, req, res, next) {
// Known application errors
if (err.code && ERROR_CODES[err.code]) {
const { status, title } = ERROR_CODES[err.code];
const baseUrl = 'https://api.example.com/errors';
const body = {
type: `${baseUrl}/${err.code.toLowerCase().replace(/_/g, '-')}`,
title,
status,
detail: err.detail || title,
instance: `/logs/${req.id}`,
code: err.code,
...err.extras,
};
// Add field-level errors for validation
if (err.fieldErrors) {
body.errors = err.fieldErrors;
}
return res.status(status)
.type('application/problem+json')
.json(body);
}
// Unknown errors — do not leak internals
console.error('Unhandled error:', err);
res.status(500)
.type('application/problem+json')
.json({
type: 'https://api.example.com/errors/internal-error',
title: 'Internal server error',
status: 500,
detail: 'An unexpected error occurred. Please try again later.',
instance: `/logs/${req.id}`,
});
}
// Usage in a route
import { NotFoundError, ValidationError } from './errors.js';
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) throw new NotFoundError('user', req.params.id);
res.json({ data: user });
});
app.post('/api/users', async (req, res) => {
const errors = validateUser(req.body);
if (errors.length > 0) throw new ValidationError(errors);
const user = await db.users.create(req.body);
res.status(201).json({ data: user });
});
app.use(errorHandler);
Localization
If your API serves clients in multiple languages, include a machine-readable code and let the client handle localization. Do not try to localize detail server-side:
{
"type": "https://api.example.com/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"errors": [
{
"field": "email",
"code": "INVALID_FORMAT",
"params": { "format": "email" }
}
]
}
The client uses code and params to look up the localized string: "El campo debe ser un correo electrónico válido."
Anti-patterns to avoid
1. Using 200 for errors
HTTP/1.1 200 OK
{ "success": false, "error": "User not found" }
This breaks HTTP semantics. Caches, proxies, and monitoring tools all rely on status codes.
2. Leaking stack traces
{
"error": "TypeError: Cannot read properties of undefined (reading 'name')",
"stack": "at UserService.getUser (/app/src/services/user.js:42:15)..."
}
Never expose internals in production. Log the full error server-side and return a generic message.
3. String-only errors
{ "error": "Invalid request" }
Clients cannot do anything useful with this. Always include a machine-readable code and field-level detail for validation errors.
4. Inconsistent shapes across endpoints
Pick one error format and use it everywhere. RFC 7807 gives you that format. Wire it into your global error handler so individual routes do not need to think about error formatting.
Mapping HTTP status codes to error types
| Status | When to use | Example type |
|---|---|---|
| 400 | Malformed request syntax | bad-request |
| 401 | Missing or invalid authentication | authentication-required |
| 403 | Authenticated but not authorized | insufficient-permissions |
| 404 | Resource does not exist | resource-not-found |
| 409 | Conflict with current state | duplicate-resource |
| 422 | Valid syntax but semantic errors | validation-failed |
| 429 | Rate limit exceeded | rate-limited |
| 500 | Unexpected server error | internal-error |
| 503 | Temporary unavailability | service-unavailable |
Summary
Use RFC 7807 Problem Details as your error response format. Set the content type to application/problem+json. Include a stable type URI, a human-readable detail, and a machine-readable code. For validation errors, add a errors array with field-level detail. Wire everything through a global error handler so every endpoint returns the same shape. Your API consumers will thank you.
Related articles
- REST APIs REST API Documentation with OpenAPI 3.1
Write machine-readable API documentation with OpenAPI 3.1. Learn the spec structure, Swagger UI setup, code generation, and best practices for keeping docs in sync.
- REST APIs REST API Hypermedia & HATEOAS: HAL, JSON:API & Practical Examples
Implement HATEOAS in your REST API using HAL, JSON:API, and custom link formats. Learn when hypermedia helps, when it hurts, and how to add it incrementally.
- REST APIs REST API Pagination Best Practices: Offset, Cursor & Keyset
Choose the right pagination strategy for your REST API. Compare offset, cursor, and keyset pagination with real examples, HATEOAS links, and performance trade-offs.
- REST APIs REST API Rate Limiting: Token Bucket, Sliding Window & Implementation
Protect your REST API from abuse with rate limiting. Learn token bucket, sliding window, and fixed window algorithms with real implementation examples and standard headers.