Chapter 4.5☕ 22 min read

Error Handling & Validation — Express Error Middleware

Error handling is like a Swiggy quality check — catch problems early (validation), handle failures gracefully (error middleware), and never serve bad biryani to the user.

01Input Validation — Biryani Order Mein Quantity 0 Se Zyada Honi Chahiye

Input validation is the process of verifying that user input meets your requirements before processing it. Think of it like checking biryani orders at Swiggy:

  • Is the email actually an email? (not "abcdef")
  • Is the biryani quantity at least 1? (not 0 or -5)
  • Is the price a positive number? (not "free" or -100)
  • Is the delivery address provided? (mandatory field)

Why validate?

  • Security — Prevents injection attacks, malformed data exploits
  • Data integrity — Ensures only clean data enters your database
  • User experience — Shows clear error messages instead of cryptic crashes
  • API reliability — Returns proper HTTP status codes (400 for bad input, 500 for server errors)

Two types of errors in Express:

  1. Operational errors — Predictable errors like invalid input, not found, unauthorized (these should return 4xx status codes)
  2. Programming errors — Bugs like database connection failure, undefined variable (these should return 500, and you should fix the code)

In this chapter, we'll use express-validator for input validation and build a centralized error handler that catches all errors in one place.

02express-validator — body(), validationResult() Use Karna

express-validator is a set of Express middleware functions that validate and sanitize incoming request data. It's built on top of the popular validator.js library.

npm install express-validator

Basic validation for a biryani order:

// middleware/validation.middleware.js — Input Validation
import { body, validationResult } from 'express-validator';

// Validation rules for placing an order
const validateOrder = [
  // Items must be an array with at least 1 item
  body('items')
    .isArray({ min: 1 })
    .withMessage('Kuch toh order karo bhai! At least 1 item chahiye!'),

  // Each item must have name, qty, price
  body('items.*.name')
    .trim()
    .notEmpty()
    .withMessage('Har item ka name daalo bhai!'),

  body('items.*.qty')
    .isInt({ min: 1 })
    .withMessage('Quantity at least 1 honi chahiye — 0 biryani kaun khaata hai bhai?'),

  body('items.*.price')
    .isFloat({ min: 1 })
    .withMessage('Price positive number daalo — free mein thodi degi restaurant?'),

  // Delivery address
  body('deliveryAddress.street')
    .trim()
    .notEmpty()
    .withMessage('Street address toh daalo bhai! Gali kya number?'),

  body('deliveryAddress.city')
    .trim()
    .notEmpty()
    .withMessage('City ka naam toh daalo bhai!'),

  body('deliveryAddress.pincode')
    .isPostalCode('IN')
    .withMessage('Sahi pincode daalo bhai! 6 digit ka hota hai'),

  // Handle validation errors
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        success: false,
        message: 'Validation failed! Form sahi bharo bhai!',
        errors: errors.array().map(err => ({
          field: err.path,
          message: err.msg
        }))
      });
    }
    next();
  }
];

export { validateOrder };

Registration validation:

// Registration validation rules
const validateRegister = [
  body('name')
    .trim()
    .isLength({ min: 2, max: 50 })
    .withMessage('Name 2 se 50 characters ke beech mein daalo bhai!'),

  body('email')
    .trim()
    .isEmail()
    .withMessage('Sahi email daalo bhai — yeh kya bhej diya?')
    .normalizeEmail(),

  body('password')
    .isLength({ min: 6 })
    .withMessage('Password kam se kam 6 characters ka hona chahiye')
    .matches(/[A-Z]/).withMessage('Password mein kam se kam ek uppercase letter hona chahiye')
    .matches(/[0-9]/).withMessage('Password mein kam se kam ek number hona chahiye'),

  body('phone')
    .optional()
    .isMobilePhone('any')
    .withMessage('Sahi phone number daalo bhai!'),

  // Error handler
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        success: false,
        message: 'Validation failed!',
        errors: errors.array()
      });
    }
    next();
  }
];

Using validation in routes:

// routes/order.routes.js — With Validation Middleware
import express from 'express';
import { validateOrder } from '../middleware/validation.middleware.js';
import { protect } from '../middleware/auth.middleware.js';
import { placeOrder } from '../controllers/order.controller.js';

const router = express.Router();

// Validation middleware runs BEFORE the controller
router.post('/', protect, validateOrder, placeOrder);
//                     ↑ auth ↑ validation ↑ handler

export default router;

express-validator common validators:

ValidatorChecksExample
isEmail()Valid email formatbody('email').isEmail()
isInt({ min, max })Integer in rangebody('qty').isInt({ min: 1 })
isFloat({ min })Float with minimumbody('price').isFloat({ min: 0 })
isLength({ min, max })String length rangebody('name').isLength({ min: 2 })
isMongoId()Valid MongoDB ObjectIdbody('id').isMongoId()
isURL()Valid URLbody('website').isURL()
isIn()Value in allowed listbody('role').isIn(['admin', 'user'])
notEmpty()Not empty stringbody('name').notEmpty()
optional()Skip if not providedbody('phone').optional().isMobilePhone()
03Centralized Error Handler — Express Error Middleware (err, req, res, next)

Centralized error handling means ALL errors — whether from validation, controllers, or async operations — are caught and handled in ONE place. No more try-catch in every route handler!

Express error middleware signature:

// Express error middleware has FOUR parameters
// If you have less than 4, Express treats it as normal middleware!
app.use((err, req, res, next) => {
  // This only runs when there's an error
  // err is the error object passed via next(err)
});

Building the centralized error handler:

// middleware/error.middleware.js — Centralized Error Handler
import config from '../config/index.js';

const errorHandler = (err, req, res, next) => {
  // Default values
  let statusCode = err.statusCode || 500;
  let message = err.message || 'Kuch to gadbad hai bhai!';

  // Log error in development (with stack trace)
  if (config.isDevelopment) {
    console.error('❌ ERROR:', err);
  } else {
    // In production, log without sensitive details
    console.error('❌ Error:', statusCode, message);
  }

  // Mongoose bad ObjectId format
  if (err.name === 'CastError') {
    statusCode = 400;
    message = 'Invalid ID format — sahi ID daalo bhai!';
  }

  // Mongoose duplicate key
  if (err.code === 11000) {
    statusCode = 409;
    const field = Object.keys(err.keyValue)[0];
    message = field + ' already exists! Yeh ' + field + ' already registered hai';
  }

  // Mongoose validation error
  if (err.name === 'ValidationError') {
    statusCode = 400;
    const messages = Object.values(err.errors).map(e => e.message);
    message = 'Validation failed: ' + messages.join('. ');
  }

  // JWT errors
  if (err.name === 'JsonWebTokenError') {
    statusCode = 401;
    message = 'Invalid token! JWT mein gadbad hai — phirse login karo bhai!';
  }

  if (err.name === 'TokenExpiredError') {
    statusCode = 401;
    message = 'Token expired! Login karo bhai! 🎫';
  }

  // Multer errors (file upload)
  if (err.name === 'MulterError') {
    statusCode = 400;
    if (err.code === 'LIMIT_FILE_SIZE') {
      message = 'File too large! Max 5MB allowed 📏';
    } else if (err.code === 'LIMIT_UNEXPECTED_FILE') {
      message = 'Unexpected file field! Check your form field name';
    }
  }

  // Send error response
  res.status(statusCode).json({
    success: false,
    message,
    ...(config.isDevelopment && { stack: err.stack }) // Stack trace only in dev
  });
};

// 404 handler — for unmatched routes
const notFoundHandler = (req, res, next) => {
  res.status(404).json({
    success: false,
    message: 'Route not found! Yeh endpoint exist nahi karta bhai!'
  });
};

export { errorHandler, notFoundHandler };

Installing error middleware in Express app:

// server.js — Installing Error Middleware
import express from 'express';
import { errorHandler, notFoundHandler } from './middleware/error.middleware.js';

const app = express();

// 1. Routes (normal middleware)
app.use('/api/auth', authRoutes);
app.use('/api/orders', orderRoutes);
app.use('/api/menu', menuRoutes);

// 2. 404 handler — catches unmatched routes (must be after routes)
app.use(notFoundHandler);

// 3. Error handler — catches ALL errors (must be last!)
app.use(errorHandler);

// ⚠️ ORDER MATTERS! Routes → 404 → Error Handler
// If error handler is before routes, it catches everything!
04Custom Error Classes — AppError: Kuch to Gadbad Hai Bhai!

Instead of throwing generic Error objects, create a custom AppError class that includes status codes and operational flags. This makes error handling cleaner and more consistent.

// utils/AppError.js — Custom Error Class
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = String(statusCode).startsWith('4') ? 'fail' : 'error';
    this.isOperational = true; // Differentiate from programming errors

    // Capture stack trace
    Error.captureStackTrace(this, this.constructor);
  }
}

export default AppError;

Using AppError in controllers:

// controllers/biryani.controller.js — Using AppError
import Biryani from '../models/Biryani.js';
import AppError from '../utils/AppError.js';

// GET /api/biryani/:id — Get single biryani
export const getBiryani = async (req, res, next) => {
  try {
    const biryani = await Biryani.findById(req.params.id);

    if (!biryani) {
      // Throw custom error with 404 status
      throw new AppError('Biryani not found! Yeh biryani exist nahi karti! 🍔', 404);
    }

    res.json({ success: true, data: biryani });
  } catch (error) {
    // Pass to error middleware
    next(error);
  }
};

// POST /api/biryani — Create biryani (admin only)
export const createBiryani = async (req, res, next) => {
  try {
    const { name, price, category } = req.body;

    // Manual validation (or use express-validator)
    if (!name || !price) {
      throw new AppError('Name and price required! Biryani ka naam aur price toh daalo bhai!', 400);
    }

    if (price < 10) {
      throw new AppError('Itni sasti biryani nahi hoti! Minimum Rs.10', 400);
    }

    const biryani = await Biryani.create(req.body);

    res.status(201).json({ success: true, data: biryani });
  } catch (error) {
    next(error);
  }
};

Async handler wrapper — no more try-catch in every route!

// utils/asyncHandler.js — Wraps async routes, catches errors automatically
const asyncHandler = (fn) => {
  return (req, res, next) => {
    // If fn returns a promise, catch rejects and pass to next()
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

export default asyncHandler;

// Usage — no try-catch needed!
import asyncHandler from '../utils/asyncHandler.js';

export const getBiryani = asyncHandler(async (req, res, next) => {
  const biryani = await Biryani.findById(req.params.id);

  if (!biryani) {
    throw new AppError('Biryani not found!', 404);
  }

  res.json({ success: true, data: biryani });
  // No try-catch! asyncHandler catches any error and calls next(error)
});

Complete error classes:

// utils/errors.js — All custom error classes
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

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

class UnauthorizedError extends AppError {
  constructor(message = 'Please login first! Login karo bhai!') {
    super(message, 401);
  }
}

class ForbiddenError extends AppError {
  constructor(message = 'Aapke paas permission nahi hai bhai!') {
    super(message, 403);
  }
}

class ValidationError extends AppError {
  constructor(message = 'Validation failed! Sahi data daalo bhai!') {
    super(message, 400);
  }
}

export { AppError, NotFoundError, UnauthorizedError, ForbiddenError, ValidationError };
05Complete Error Handling Flow — Validation → Controller → Error Middleware

Let's put it all together — from validation to error middleware. This is the complete error handling flow of a production Express app.

Complete Error Handling Flow:

Request → Validation Middleware → Controller → Response
                        ↓ (error)              ↓ (error)
                    400 Bad Request     Centralized Error Middleware
                                            ↓
                                      Formatted Error Response

Complete server.js with all error handling:

// server.js — Complete with Error Handling
import 'dotenv/config';
import express from 'express';
import config from './config/index.js';
import { errorHandler, notFoundHandler } from './middleware/error.middleware.js';

const app = express();

// Body parser
app.use(express.json());

// ─── Routes ──────────────────────────────────────
app.use('/api/auth', authRoutes);
app.use('/api/orders', orderRoutes);
app.use('/api/menu', menuRoutes);
app.use('/api/upload', uploadRoutes);

// ─── 404 Handler (after all routes) ──────────────
app.use(notFoundHandler);

// ─── Global Error Handler (always last!) ─────────
app.use(errorHandler);

// ─── Start Server ────────────────────────────────
const start = async () => {
  try {
    await mongoose.connect(config.mongo.uri);
    app.listen(config.port, () => {
      console.log('🚀 Server running on port ' + config.port);
    });
  } catch (err) {
    console.error('❌ Failed to start:', err.message);
    process.exit(1);
  }
};

start();

Complete validation + controller + error flow example:

// routes/order.routes.js — Complete Validation Flow
import express from 'express';
import { body, validationResult } from 'express-validator';
import asyncHandler from '../utils/asyncHandler.js';
import AppError from '../utils/AppError.js';
import { protect } from '../middleware/auth.middleware.js';

const router = express.Router();

// Validation rules for creating order
const validateCreateOrder = [
  body('items').isArray({ min: 1 }).withMessage('At least 1 item required!'),
  body('items.*.name').trim().notEmpty().withMessage('Item name required!'),
  body('items.*.qty').isInt({ min: 1 }).withMessage('Qty must be >= 1'),
  body('items.*.price').isFloat({ min: 1 }).withMessage('Price must be > 0'),

  // Error handler for validation
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        success: false,
        message: 'Validation failed!',
        errors: errors.array()
      });
    }
    next();
  }
];

// Protected + validated route
router.post('/', protect, validateCreateOrder, asyncHandler(async (req, res, next) => {
  const { items, deliveryAddress } = req.body;

  // Check if items in stock (operational error)
  for (const item of items) {
    const biryani = await Biryani.findById(item.id);
    if (!biryani) {
      throw new AppError(item.name + ' abhi available nahi hai! 🍔', 404);
    }
    if (!biryani.isAvailable) {
      throw new AppError(biryani.name + ' out of stock! Koi aur order karo 🚫', 400);
    }
  }

  const order = await Order.create({
    user: req.user.id,
    items,
    deliveryAddress,
    total: items.reduce((sum, i) => sum + (i.price * i.qty), 0),
    status: 'confirmed'
  });

  res.status(201).json({ success: true, data: order });
}));

export default router;

What happens in each error scenario:

ScenarioWhere Error is CaughtResponse
Invalid email formatValidation middleware400 with field-level errors
Missing required fieldValidation middleware400 with field-level errors
Biryani not found (wrong ID)Controller → throws AppError → next(error) → error middleware404 — "Biryani not found!"
Insufficient stockController → throws AppError → next(error) → error middleware400 — "Out of stock!"
Mongoose duplicate keyError middleware detects err.code 11000409 — "Already exists!"
Invalid MongoDB ObjectIdError middleware detects CastError400 — "Invalid ID"
JWT expiredError middleware detects TokenExpiredError401 — "Login again"
Unknown route404 handler (catch-all)404 — "Route not found"
Server crashes (programming bug)Error middleware (catch-all)500 — "Kuch to gadbad hai bhai!"

Key Takeaways

  • ✅ Input validation prevents bad data, security exploits, and provides clear user feedback
  • ✅ express-validator: body().isEmail().withMessage() + validationResult() for field-level validation
  • ✅ Validation middleware runs BEFORE controller — catch bad input early
  • ✅ Express error middleware has 4 params: (err, req, res, next) — catches ALL errors
  • ✅ Error middleware must be the LAST middleware in your Express app
  • ✅ Custom AppError class with statusCode makes error handling consistent
  • ✅ asyncHandler wrapper catches async errors without try-catch in every route
  • ✅ Differentiate: 4xx = client errors (validation, not found), 5xx = server errors (bugs)
  • ✅ next(err) passes errors to the centralized error middleware
  • ✅ Never return 200 OK when validation fails — always use appropriate 4xx status codes
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase