Consistent Error Format

Design One Error Shape

Frontend and mobile developers love APIs where every error looks the same. A common shape:

{
  "success": false,
  "error": {
    "statusCode": 422,
    "code": "OUT_OF_STOCK",
    "message": "Only 2 left for product 7",
    "details": { "productId": 7, "available": 2 },
    "path": "/api/orders",
    "requestId": "0b9a5c1e"
  },
  "timestamp": "2026-01-15T10:00:00.000Z"
}
  • code: a stable machine-readable string the app can switch on (messages may change or be translated).
  • message: human-readable, safe to show.
  • details: optional extra data (validation errors per field).
  • requestId: links the error to server logs.

Normalising Any Error

Example

The function at the heart of such a filter: accept anything that was thrown and always produce the same shape.

type HttpLike = { status: number; response: string | { message?: string | string[]; code?: string } };
const isHttp = (e: unknown): e is HttpLike => typeof e === 'object' && e !== null && 'status' in e && 'response' in e;

function normalise(e: unknown, path: string) {
  let statusCode = 500;
  let code = 'INTERNAL';
  let message: string | string[] = 'Internal server error';
  if (isHttp(e)) {
    statusCode = e.status;
    if (typeof e.response === 'string') message = e.response;
    else {
      message = e.response.message ?? message;
      code = e.response.code ?? (Array.isArray(message) ? 'VALIDATION_FAILED' : 'HTTP_' + statusCode);
    }
    if (code === 'INTERNAL') code = 'HTTP_' + statusCode;
  }
  return { success: false, error: { statusCode, code, message, path } };
}

console.log(JSON.stringify(normalise({ status: 404, response: 'Product 999 not found' }, '/products/999')));
console.log(JSON.stringify(normalise({ status: 400, response: { message: ['price must be a positive number'] } }, '/products')));
console.log(JSON.stringify(normalise({ status: 422, response: { code: 'OUT_OF_STOCK', message: 'Only 2 left' } }, '/orders')));
console.log(JSON.stringify(normalise(new Error('connection refused'), '/orders')));

Output

{"success":false,"error":{"statusCode":404,"code":"HTTP_404","message":"Product 999 not found","path":"/products/999"}}
{"success":false,"error":{"statusCode":400,"code":"VALIDATION_FAILED","message":["price must be a positive number"],"path":"/products"}}
{"success":false,"error":{"statusCode":422,"code":"OUT_OF_STOCK","message":"Only 2 left","path":"/orders"}}
{"success":false,"error":{"statusCode":500,"code":"INTERNAL","message":"Internal server error","path":"/orders"}}