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.
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:
- Operational errors — Predictable errors like invalid input, not found, unauthorized (these should return 4xx status codes)
- 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.
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:
| Validator | Checks | Example |
|---|---|---|
| isEmail() | Valid email format | body('email').isEmail() |
| isInt({ min, max }) | Integer in range | body('qty').isInt({ min: 1 }) |
| isFloat({ min }) | Float with minimum | body('price').isFloat({ min: 0 }) |
| isLength({ min, max }) | String length range | body('name').isLength({ min: 2 }) |
| isMongoId() | Valid MongoDB ObjectId | body('id').isMongoId() |
| isURL() | Valid URL | body('website').isURL() |
| isIn() | Value in allowed list | body('role').isIn(['admin', 'user']) |
| notEmpty() | Not empty string | body('name').notEmpty() |
| optional() | Skip if not provided | body('phone').optional().isMobilePhone() |
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!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 };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:
| Scenario | Where Error is Caught | Response |
|---|---|---|
| Invalid email format | Validation middleware | 400 with field-level errors |
| Missing required field | Validation middleware | 400 with field-level errors |
| Biryani not found (wrong ID) | Controller → throws AppError → next(error) → error middleware | 404 — "Biryani not found!" |
| Insufficient stock | Controller → throws AppError → next(error) → error middleware | 400 — "Out of stock!" |
| Mongoose duplicate key | Error middleware detects err.code 11000 | 409 — "Already exists!" |
| Invalid MongoDB ObjectId | Error middleware detects CastError | 400 — "Invalid ID" |
| JWT expired | Error middleware detects TokenExpiredError | 401 — "Login again" |
| Unknown route | 404 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
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login