Skip to main content
Course/Module 2/Topic 4 of 4Intermediate

Error Handling

Implement proper error handling and validation in Express.js applications

40 min•By Priygop Team•Last updated: Feb 2026

Error Handling in Express.js

Error handling is crucial for building robust Express.js applications. Express.js provides built-in error handling mechanisms and allows you to create custom error handlers for different scenarios.

Built-in Error Handling

Example
const express = require('express');
const app = express();

// Error handling middleware (must be last)
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ error: 'Something went wrong!' });
});

// Async error handling
app.get('/async', async (req, res, next) => {
    try {
        const result = await someAsyncOperation();
        res.json(result);
    } catch (error) {
        next(error); // Pass error to error handling middleware
    }
});

// Express 5.0+ async error handling
app.get('/async-v5', async (req, res) => {
    const result = await someAsyncOperation();
    res.json(result);
    // Errors are automatically caught and passed to error middleware
});

// Custom error class
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
        this.isOperational = true;

        Error.captureStackTrace(this, this.constructor);
    }
}

// Using custom error class
app.get('/users/:id', (req, res, next) => {
    const userId = req.params.id;
    
    if (!userId || isNaN(userId)) {
        return next(new AppError('Invalid user ID', 400));
    }
    
    // Process user logic
    res.json({ message: 'User found', id: userId });
});

// Error handling middleware for custom errors
app.use((err, req, res, next) => {
    err.statusCode = err.statusCode || 500;
    err.status = err.status || 'error';

    if (process.env.NODE_ENV === 'development') {
        res.status(err.statusCode).json({
            status: err.status,
            error: err,
            message: err.message,
            stack: err.stack
        });
    } else {
        // Production error response
        if (err.isOperational) {
            res.status(err.statusCode).json({
                status: err.status,
                message: err.message
            });
        } else {
            // Programming or unknown errors
            console.error('ERROR šŸ’„', err);
            res.status(500).json({
                status: 'error',
                message: 'Something went wrong!'
            });
        }
    }
});

Input Validation

Example
const express = require('express');
const app = express();

app.use(express.json());

// Manual validation
app.post('/users', (req, res, next) => {
    const { name, email, age } = req.body;
    
    // Validation
    if (!name || name.trim().length < 2) {
        return res.status(400).json({ error: 'Name must be at least 2 characters long' });
    }
    
    if (!email || !email.includes('@')) {
        return res.status(400).json({ error: 'Valid email is required' });
    }
    
    if (age && (age < 0 || age > 120)) {
        return res.status(400).json({ error: 'Age must be between 0 and 120' });
    }
    
    // Process valid data
    res.status(201).json({ message: 'User created successfully' });
});

// Using validation libraries
const { body, validationResult } = require('express-validator');

app.post('/users-validated', [
    body('name').isLength({ min: 2 }).withMessage('Name must be at least 2 characters'),
    body('email').isEmail().withMessage('Must be a valid email'),
    body('age').optional().isInt({ min: 0, max: 120 }).withMessage('Age must be between 0 and 120')
], (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }
    
    res.status(201).json({ message: 'User created successfully' });
});

// Sanitization
const { body, sanitizeBody } = require('express-validator');

app.post('/users-sanitized', [
    body('name').trim().escape(),
    body('email').normalizeEmail(),
    sanitizeBody('age').toInt()
], (req, res) => {
    // Data is now sanitized
    res.status(201).json({ message: 'User created', data: req.body });
});

Advanced Error Handling Patterns

Example
// Advanced error handling patterns for Express.js
const express = require('express');
const app = express();

// Custom error classes
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;
        Error.captureStackTrace(this, this.constructor);
    }
}

class ValidationError extends AppError {
    constructor(message, details = []) {
        super(message, 400);
        this.details = details;
    }
}

class NotFoundError extends AppError {
    constructor(resource = 'Resource') {
        super(`${resource} not found`, 404);
    }
}

// Error handling middleware
const errorHandler = (err, req, res, next) => {
    let error = { ...err };
    error.message = err.message;

    // Log error
    console.error('Error:', err);

    // Mongoose bad ObjectId
    if (err.name === 'CastError') {
        const message = 'Resource not found';
        error = new AppError(message, 404);
    }

    // Mongoose duplicate key
    if (err.code === 11000) {
        const message = 'Duplicate field value entered';
        error = new AppError(message, 400);
    }

    // Mongoose validation error
    if (err.name === 'ValidationError') {
        const message = Object.values(err.errors).map(val => val.message).join(', ');
        error = new ValidationError(message);
    }

    // JWT errors
    if (err.name === 'JsonWebTokenError') {
        const message = 'Invalid token';
        error = new AppError(message, 401);
    }

    if (err.name === 'TokenExpiredError') {
        const message = 'Token expired';
        error = new AppError(message, 401);
    }

    res.status(error.statusCode || 500).json({
        success: false,
        error: error.message || 'Server Error',
        ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
        ...(error.details && { details: error.details })
    });
};

// Async error wrapper
const asyncHandler = (fn) => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

// Usage examples
app.get('/users/:id', asyncHandler(async (req, res, next) => {
    const user = await User.findById(req.params.id);
    
    if (!user) {
        return next(new NotFoundError('User'));
    }
    
    res.json({ success: true, data: user });
}));

app.post('/users', asyncHandler(async (req, res, next) => {
    const { name, email } = req.body;
    
    if (!name || !email) {
        return next(new ValidationError('Name and email are required'));
    }
    
    const user = await User.create({ name, email });
    res.status(201).json({ success: true, data: user });
}));

// Global error handler
app.use(errorHandler);

module.exports = app;

Error Handling Best Practices

  • Use custom error classes: Create specific error types for different scenarios
  • Implement global error handler: Centralize error handling logic
  • Log errors properly: Use structured logging with appropriate levels
  • Don't expose sensitive information: Sanitize error messages in production
  • Use appropriate HTTP status codes: Match status codes to error types
  • Handle async errors: Use try-catch or async error wrappers
  • Implement error monitoring: Use services like Sentry for production
  • Create error recovery strategies: Implement retry logic where appropriate

Mini-Project: comprehensive Error Handling System

Example
// Complete error handling system for Express.js application
const express = require('express');
const winston = require('winston');
const Sentry = require('@sentry/node');

const app = express();

// Configure logging
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' }),
        new winston.transports.Console({
            format: winston.format.simple()
        })
    ]
});

// Initialize Sentry for error monitoring
Sentry.init({
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV || 'development'
});

// Custom error classes
class AppError extends Error {
    constructor(message, statusCode, isOperational = true) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = isOperational;
        this.timestamp = new Date().toISOString();
        Error.captureStackTrace(this, this.constructor);
    }
}

class ValidationError extends AppError {
    constructor(message, field = null) {
        super(message, 400);
        this.field = field;
        this.type = 'ValidationError';
    }
}

class AuthenticationError extends AppError {
    constructor(message = 'Authentication failed') {
        super(message, 401);
        this.type = 'AuthenticationError';
    }
}

class AuthorizationError extends AppError {
    constructor(message = 'Insufficient permissions') {
        super(message, 403);
        this.type = 'AuthorizationError';
    }
}

class NotFoundError extends AppError {
    constructor(resource = 'Resource') {
        super(`${resource} not found`, 404);
        this.type = 'NotFoundError';
    }
}

class ConflictError extends AppError {
    constructor(message = 'Resource conflict') {
        super(message, 409);
        this.type = 'ConflictError';
    }
}

class RateLimitError extends AppError {
    constructor(message = 'Too many requests') {
        super(message, 429);
        this.type = 'RateLimitError';
    }
}

// Error handling utilities
const handleAsync = (fn) => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

const handleValidation = (schema) => (req, res, next) => {
    const { error } = schema.validate(req.body);
    if (error) {
        const details = error.details.map(detail => ({
            field: detail.path.join('.'),
            message: detail.message
        }));
        return next(new ValidationError('Validation failed', details));
    }
    next();
};

// Error response formatter
const formatErrorResponse = (err, req) => {
    const response = {
        success: false,
        error: {
            message: err.message,
            type: err.type || 'UnknownError',
            timestamp: err.timestamp || new Date().toISOString(),
            requestId: req.requestId || 'unknown'
        }
    };

    // Add additional details in development
    if (process.env.NODE_ENV === 'development') {
        response.error.stack = err.stack;
        response.error.details = err.details;
    }

    return response;
};

// Global error handler
const errorHandler = (err, req, res, next) => {
    let error = { ...err };
    error.message = err.message;

    // Log error
    logger.error({
        message: err.message,
        stack: err.stack,
        requestId: req.requestId,
        url: req.url,
        method: req.method,
        ip: req.ip,
        userAgent: req.get('User-Agent')
    });

    // Send to Sentry
    Sentry.captureException(err);

    // Handle specific error types
    if (err.name === 'CastError') {
        error = new NotFoundError('Resource');
    }

    if (err.code === 11000) {
        const field = Object.keys(err.keyValue)[0];
        error = new ConflictError(`${field} already exists`);
    }

    if (err.name === 'ValidationError') {
        const details = Object.values(err.errors).map(val => ({
            field: val.path,
            message: val.message
        }));
        error = new ValidationError('Validation failed', details);
    }

    if (err.name === 'JsonWebTokenError') {
        error = new AuthenticationError('Invalid token');
    }

    if (err.name === 'TokenExpiredError') {
        error = new AuthenticationError('Token expired');
    }

    // Send error response
    const statusCode = error.statusCode || 500;
    const response = formatErrorResponse(error, req);
    
    res.status(statusCode).json(response);
};

// 404 handler
const notFoundHandler = (req, res, next) => {
    const error = new NotFoundError(`Route ${req.originalUrl}`);
    next(error);
};

// Rate limiting error handler
const rateLimitHandler = (req, res) => {
    const error = new RateLimitError('Too many requests, please try again later');
    const response = formatErrorResponse(error, req);
    res.status(429).json(response);
};

// Apply middleware
app.use(Sentry.requestHandler());
app.use(express.json());

// Request ID middleware
app.use((req, res, next) => {
    req.requestId = Math.random().toString(36).substr(2, 9);
    res.setHeader('X-Request-ID', req.requestId);
    next();
});

// Example routes with error handling
app.get('/users/:id', handleAsync(async (req, res, next) => {
    const { id } = req.params;
    
    if (!id || isNaN(id)) {
        throw new ValidationError('Invalid user ID');
    }
    
    // Simulate database operation
    const user = await simulateDatabaseOperation('findUser', id);
    
    if (!user) {
        throw new NotFoundError('User');
    }
    
    res.json({ success: true, data: user });
}));

app.post('/users', handleValidation(userSchema), handleAsync(async (req, res, next) => {
    const { name, email } = req.body;
    
    // Simulate database operation
    const existingUser = await simulateDatabaseOperation('findUserByEmail', email);
    
    if (existingUser) {
        throw new ConflictError('User with this email already exists');
    }
    
    const user = await simulateDatabaseOperation('createUser', { name, email });
    
    res.status(201).json({ success: true, data: user });
}));

// Simulate database operations
async function simulateDatabaseOperation(operation, data) {
    // Simulate async operation
    await new Promise(resolve => setTimeout(resolve, 100));
    
    switch (operation) {
        case 'findUser':
            return data === '1' ? { id: 1, name: 'John Doe', email: 'john@example.com' } : null;
        case 'findUserByEmail':
            return data === 'john@example.com' ? { id: 1, name: 'John Doe', email: 'john@example.com' } : null;
        case 'createUser':
            return { id: Date.now(), ...data, createdAt: new Date() };
        default:
            return null;
    }
}

// User validation schema (using Joi)
const Joi = require('joi');
const userSchema = Joi.object({
    name: Joi.string().min(2).max(50).required(),
    email: Joi.string().email().required(),
    age: Joi.number().integer().min(0).max(150).optional()
});

// Apply error handlers
app.use(notFoundHandler);
app.use(errorHandler);
app.use(Sentry.errorHandler());

// Graceful shutdown
process.on('SIGTERM', () => {
    logger.info('SIGTERM received, shutting down gracefully');
    process.exit(0);
});

process.on('SIGINT', () => {
    logger.info('SIGINT received, shutting down gracefully');
    process.exit(0);
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    logger.info(`Server running on port ${PORT}`);
});

module.exports = app;

Try It Yourself — Express.js Framework

Try It Yourself — Express.js FrameworkHTML
HTML Editor
āœ“ ValidTab = 2 spaces
HTML|39 lines|1938 chars|āœ“ Valid syntax
UTF-8

Quick Quiz — Express.js Framework

Additional Resources

Recommended Reading

  • • Express.js Official Documentation
  • • Express.js Routing Guide
  • • Middleware in Express.js

Online Resources

  • • Express.js Tutorial for Beginners
  • • REST API Design with Express.js
  • • Express.js Best Practices
Chat on WhatsApp
Priygop - Leading Professional Development Platform | Expert Courses & Interview Prep