Skip to main content

API Error Handling: RFC 7807 Problem Details & Best Practices

Consistent, informative error responses are a mark of a well-designed API. This guide covers the HTTP status code vocabulary, the RFC 7807 Problem Details standard, validation error patterns, and Express middleware for centralizing error handling.

✓ HTTP status code reference ✓ RFC 7807 Problem Details format ✓ Validation error arrays ✓ Express central error handler

HTTP Status Code Quick Reference

Code Name When to Use
200OKSuccessful GET, PUT, PATCH
201CreatedPOST created a resource (add Location header)
204No ContentDELETE succeeded, no body returned
400Bad RequestMalformed JSON, missing required params
401UnauthorizedMissing or invalid authentication token
403ForbiddenAuthenticated but lacks permission
404Not FoundResource does not exist
409ConflictDuplicate create, version conflict
422Unprocessable EntityValidation errors on well-formed input
429Too Many RequestsRate limit exceeded (add Retry-After)
500Internal Server ErrorUnexpected server failure
503Service UnavailableDownstream dependency is down

RFC 7807 Problem Details Format

Use Content-Type: application/problem+json and structure errors as standardized Problem Details objects. Extend with custom fields as needed:

// HTTP 422 Unprocessable Entity
// Content-Type: application/problem+json

{
  "type": "https://devplaybook.cc/problems/validation-error",
  "title": "Validation Failed",
  "status": 422,
  "detail": "The request body contains invalid fields.",
  "instance": "/requests/f7ab2c10-a1b2-4c3d",
  "errors": [
    {
      "pointer": "/data/attributes/email",
      "detail": "Must be a valid email address",
      "code": "invalid_email"
    },
    {
      "pointer": "/data/attributes/age",
      "detail": "Must be a positive integer",
      "code": "invalid_range"
    }
  ]
}

Express Central Error Handler

Centralize all error formatting in a single Express error middleware registered last. Custom error classes carry status codes and structured detail:

// errors/AppError.ts
export class AppError extends Error {
  constructor(
    public readonly status: number,
    public readonly type: string,
    message: string,
    public readonly errors?: object[],
  ) { super(message); }
}

export class ValidationError extends AppError {
  constructor(errors: { pointer: string; detail: string; code: string }[]) {
    super(422, 'https://devplaybook.cc/problems/validation-error',
      'Validation Failed', errors);
  }
}

// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';

export function errorHandler(err: any, req: Request, res: Response, _next: NextFunction) {
  // Never expose stack traces in production
  if (process.env.NODE_ENV !== 'production') console.error(err);

  const status = err.status ?? 500;
  const body: Record<string, unknown> = {
    type: err.type ?? 'https://devplaybook.cc/problems/internal-error',
    title: err.message ?? 'An unexpected error occurred',
    status,
    instance: req.path,
  };

  if (err.errors) body.errors = err.errors;

  // Log internal errors with correlation ID
  if (status >= 500) {
    const correlationId = req.headers['x-correlation-id'] ?? crypto.randomUUID();
    console.error({ correlationId, error: err.message, stack: err.stack });
    body.correlationId = correlationId;
    body.title = 'An unexpected error occurred';  // sanitize message
  }

  res.status(status).contentType('application/problem+json').json(body);
}
Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$9/mo after trial · Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →

Frequently Asked Questions

What HTTP status codes should my API use?▼

Use 200 OK for successful GET/PUT/PATCH, 201 Created for POST that creates a resource (include a Location header), 204 No Content for DELETE. For errors: 400 Bad Request for malformed input, 401 Unauthorized for missing/invalid auth, 403 Forbidden for insufficient permissions, 404 Not Found for missing resources, 409 Conflict for duplicate creates, 422 Unprocessable Entity for validation errors, 429 Too Many Requests for rate limits, 500 Internal Server Error for unexpected failures.

What is RFC 7807 Problem Details?▼

RFC 7807 (now superseded by RFC 9457) is an IETF standard for structuring HTTP API error responses. It defines a JSON object with type (a URI identifying the error type), title (human-readable summary), status (HTTP status code), detail (specific explanation), and instance (a URI identifying this specific error occurrence). Using a standard format means clients can parse errors generically across different APIs.

Should I return validation errors in a single response or one at a time?▼

Always return all validation errors at once. Returning one error at a time forces the user or client to fix, resubmit, and discover the next error in a frustrating loop. Collect all validation failures, return them in an errors array in a single 422 response, and make each error object identify the specific field (via a pointer or field name) and the reason it failed.

Should I expose stack traces in API error responses?▼

Never in production. Stack traces expose your internal implementation details, file paths, library versions, and logic flow to potential attackers. Log the full stack trace server-side with a correlation ID, and return only that correlation ID in the error response so your team can look up the details. The error message returned to clients should describe what went wrong, not how your code works.

What is the difference between 401 and 403?▼

401 Unauthorized means the request lacks valid credentials — the client is not authenticated. The client should retry with a valid token or credentials. 403 Forbidden means the client is authenticated but does not have permission to perform the action — re-authenticating will not help. A common mistake is returning 401 for permission failures, which confuses clients into attempting re-authentication unnecessarily.

How should GraphQL APIs handle errors?▼

GraphQL uses a top-level errors array alongside the data field. A request can partially succeed — data contains the successful parts and errors lists the failures with message, locations (line/column in the query), path (which field failed), and extensions for additional metadata like error codes. For application-level errors that should be part of the type system, use union types (e.g. CreateUserResult = User | ValidationError | AuthError) so clients get typed error handling.