Chuyển tới nội dung chính

Error Handler Middleware

Global Express error handling middleware with structured error responses, logging, and different error type handling.

Overview

graph LR
A[Route Handler] -->|throw| B[Error Handler]
B --> C{Error Type}
C -->|Validation| D[400 Bad Request]
C -->|Auth| E[401 Unauthorized]
C -->|NotFound| F[404 Not Found]
C -->|Rate Limit| G[429 Too Many]
C -->|Server| H[500 Internal]

D --> I[JSON Response]
E --> I
F --> I
G --> I
H --> I

Features

FeatureDescription
Structured ErrorsConsistent JSON error format
Error TypesDifferent handling per error type
Stack TracesOnly in development mode
LoggingWinston integration
Request ContextInclude request details

Implementation

// middlewares/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { logger } from '../utils/logger';

export interface AppError extends Error {
statusCode?: number;
code?: string;
details?: any;
isOperational?: boolean;
}

export function errorHandler(
err: AppError,
req: Request,
res: Response,
next: NextFunction
): void {
// Default values
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
const code = err.code || 'INTERNAL_ERROR';

// Log error
const logContext = {
method: req.method,
url: req.originalUrl,
statusCode,
code,
stack: err.stack,
body: req.body,
query: req.query
};

if (statusCode >= 500) {
logger.error(`Server Error: ${message}`, logContext);
} else {
logger.warn(`Client Error: ${message}`, logContext);
}

// Send response
res.status(statusCode).json({
error: {
message,
code,
...(err.details && { details: err.details }),
...(process.env.NODE_ENV === 'development' && {
stack: err.stack
})
},
timestamp: new Date().toISOString(),
path: req.originalUrl
});
}

Custom Error Classes

// errors/AppError.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code: string = 'INTERNAL_ERROR',
public details?: any
) {
super(message);
this.name = 'AppError';
Error.captureStackTrace(this, this.constructor);
}
}

// Specialized errors
export class ValidationError extends AppError {
constructor(message: string, details?: any) {
super(message, 400, 'VALIDATION_ERROR', details);
}
}

export class NotFoundError extends AppError {
constructor(resource: string, id?: string) {
super(
id ? `${resource} not found: ${id}` : `${resource} not found`,
404,
'NOT_FOUND'
);
}
}

export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401, 'UNAUTHORIZED');
}
}

export class RateLimitError extends AppError {
constructor(retryAfter?: number) {
super('Rate limit exceeded', 429, 'RATE_LIMIT', { retryAfter });
}
}

export class ServiceUnavailableError extends AppError {
constructor(service: string) {
super(`Service unavailable: ${service}`, 503, 'SERVICE_UNAVAILABLE');
}
}

Usage in Routes

import { NotFoundError, ValidationError } from '../errors/AppError';

// Throwing errors
router.get('/cameras/:id', async (req, res, next) => {
try {
const camera = await stellioService.getEntity(req.params.id);

if (!camera) {
throw new NotFoundError('Camera', req.params.id);
}

res.json(camera);
} catch (error) {
next(error); // Pass to error handler
}
});

// Validation errors
router.post('/cameras', async (req, res, next) => {
try {
if (!req.body.name) {
throw new ValidationError('Camera name is required');
}

// Create camera...
} catch (error) {
next(error);
}
});

Error Response Format

{
"error": {
"message": "Camera not found: urn:ngsi-ld:Camera:001",
"code": "NOT_FOUND",
"details": null
},
"timestamp": "2025-11-29T10:30:00.000Z",
"path": "/api/cameras/urn:ngsi-ld:Camera:001"
}

Development Mode (with stack trace)

{
"error": {
"message": "Database connection failed",
"code": "SERVICE_UNAVAILABLE",
"stack": "Error: Database connection failed\n at Neo4jService.connect..."
},
"timestamp": "2025-11-29T10:30:00.000Z",
"path": "/api/correlation/graph"
}

Async Handler Wrapper

// utils/asyncHandler.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';

export function asyncHandler(fn: RequestHandler): RequestHandler {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}

// Usage
router.get('/cameras', asyncHandler(async (req, res) => {
const cameras = await stellioService.getEntities('Camera');
res.json(cameras);
// Errors automatically passed to error handler
}));

Registration

// server.ts
import express from 'express';
import { errorHandler } from './middlewares/errorHandler';

const app = express();

// Routes
app.use('/api/cameras', cameraRoutes);
app.use('/api/weather', weatherRoutes);
// ... other routes

// 404 handler
app.use((req, res, next) => {
next(new NotFoundError('Route'));
});

// Error handler (MUST be last)
app.use(errorHandler);

HTTP Status Codes

CodeError ClassDescription
400ValidationErrorInvalid request parameters
401UnauthorizedErrorMissing/invalid authentication
403ForbiddenErrorAccess denied
404NotFoundErrorResource not found
429RateLimitErrorToo many requests
500AppError (default)Internal server error
503ServiceUnavailableErrorExternal service down

References