Good error handling makes APIs easier to debug and integrate with.
Consistent Error Format#
1interface ApiError {
2 code: string; // Machine-readable code
3 message: string; // Human-readable message
4 details?: unknown; // Additional context
5 requestId?: string; // For support/debugging
6}
7
8// Example response
9{
10 "error": {
11 "code": "VALIDATION_ERROR",
12 "message": "Invalid request parameters",
13 "details": {
14 "email": "Must be a valid email address",
15 "age": "Must be at least 18"
16 },
17 "requestId": "req_abc123"
18 }
19}HTTP Status Codes#
1// Use appropriate status codes
2const STATUS = {
3 OK: 200,
4 CREATED: 201,
5 NO_CONTENT: 204,
6 BAD_REQUEST: 400,
7 UNAUTHORIZED: 401,
8 FORBIDDEN: 403,
9 NOT_FOUND: 404,
10 CONFLICT: 409,
11 UNPROCESSABLE_ENTITY: 422,
12 TOO_MANY_REQUESTS: 429,
13 INTERNAL_ERROR: 500,
14 SERVICE_UNAVAILABLE: 503,
15};Custom Error Classes#
1class AppError extends Error {
2 constructor(
3 public code: string,
4 message: string,
5 public statusCode: number = 500,
6 public details?: unknown
7 ) {
8 super(message);
9 this.name = 'AppError';
10 }
11}
12
13class ValidationError extends AppError {
14 constructor(details: Record<string, string>) {
15 super('VALIDATION_ERROR', 'Invalid request parameters', 422, details);
16 }
17}
18
19class NotFoundError extends AppError {
20 constructor(resource: string) {
21 super('NOT_FOUND', `${resource} not found`, 404);
22 }
23}
24
25class UnauthorizedError extends AppError {
26 constructor(message = 'Authentication required') {
27 super('UNAUTHORIZED', message, 401);
28 }
29}Error Handling Middleware#
1// Express error handler
2function errorHandler(
3 err: Error,
4 req: Request,
5 res: Response,
6 next: NextFunction
7) {
8 const requestId = req.headers['x-request-id'] || generateId();
9
10 // Log full error internally
11 logger.error({
12 requestId,
13 error: err.message,
14 stack: err.stack,
15 path: req.path,
16 method: req.method,
17 });
18
19 // Handle known errors
20 if (err instanceof AppError) {
21 return res.status(err.statusCode).json({
22 error: {
23 code: err.code,
24 message: err.message,
25 details: err.details,
26 requestId,
27 },
28 });
29 }
30
31 // Handle unknown errors (don't expose internals)
32 res.status(500).json({
33 error: {
34 code: 'INTERNAL_ERROR',
35 message: 'An unexpected error occurred',
36 requestId,
37 },
38 });
39}Validation Errors#
1import { z } from 'zod';
2
3const userSchema = z.object({
4 email: z.string().email(),
5 age: z.number().min(18),
6 name: z.string().min(2),
7});
8
9function validateRequest(schema: z.ZodSchema) {
10 return (req: Request, res: Response, next: NextFunction) => {
11 const result = schema.safeParse(req.body);
12
13 if (!result.success) {
14 const details = result.error.flatten().fieldErrors;
15 throw new ValidationError(
16 Object.fromEntries(
17 Object.entries(details).map(([key, errors]) => [
18 key,
19 errors?.join(', ') ?? 'Invalid value',
20 ])
21 )
22 );
23 }
24
25 req.body = result.data;
26 next();
27 };
28}Rate Limiting Errors#
1class RateLimitError extends AppError {
2 constructor(retryAfter: number) {
3 super(
4 'RATE_LIMIT_EXCEEDED',
5 'Too many requests. Please try again later.',
6 429,
7 { retryAfter }
8 );
9 }
10}
11
12// Include retry-after header
13if (err instanceof RateLimitError) {
14 res.setHeader('Retry-After', err.details.retryAfter);
15}Async Error Wrapper#
1// Wrap async handlers to catch errors
2const asyncHandler = (fn: RequestHandler): RequestHandler => {
3 return (req, res, next) => {
4 Promise.resolve(fn(req, res, next)).catch(next);
5 };
6};
7
8// Usage
9app.get('/users/:id', asyncHandler(async (req, res) => {
10 const user = await db.users.findUnique({ where: { id: req.params.id } });
11
12 if (!user) {
13 throw new NotFoundError('User');
14 }
15
16 res.json(user);
17}));Client-Side Error Handling#
1async function fetchApi<T>(url: string): Promise<T> {
2 const response = await fetch(url);
3
4 if (!response.ok) {
5 const error = await response.json();
6
7 switch (error.error.code) {
8 case 'UNAUTHORIZED':
9 // Redirect to login
10 window.location.href = '/login';
11 break;
12 case 'VALIDATION_ERROR':
13 // Show field errors
14 throw new ValidationError(error.error.details);
15 case 'RATE_LIMIT_EXCEEDED':
16 // Show retry message
17 const retryAfter = error.error.details.retryAfter;
18 throw new Error(`Please wait ${retryAfter} seconds`);
19 default:
20 throw new Error(error.error.message);
21 }
22 }
23
24 return response.json();
25}Use consistent error formats, appropriate status codes, and never expose internal details to clients.