Skip to content
Home JavaScript Express.js Middleware: Missing next() = 30-Second Timeouts

Express.js Middleware: Missing next() = 30-Second Timeouts

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Node.js → Topic 5 of 18
Missing next() in audit middleware caused 30-second 504 timeouts at 500+ req/s.
⚙️ Intermediate — basic JavaScript knowledge assumed
In this tutorial, you'll learn
Missing next() in audit middleware caused 30-second 504 timeouts at 500+ req/s.
  • Middleware is a sequential pipeline — Express processes every app.use() call in the order it was registered, top to bottom. Order isn't a style choice; it's logic.
  • Every middleware function must either call next() to continue the chain or send a response to terminate it — doing neither causes the client request to hang silently.
  • Error-handling middleware is identified solely by its four-parameter signature (err, req, res, next). Register it last in your file, after all routes and regular middleware.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • Middleware functions run in the order they are registered: sequential pipeline, not random.
  • Every middleware must either call next() or send a response — failure to do so hangs the request.
  • Use app.use() for global middleware, router-level for scoped concerns, and error handlers with 4 parameters.
  • req is a shared object across the chain — attach user data there, not in global variables.
  • In Express 4, async errors must be caught and forwarded with next(err); Express 5 will handle them automatically.
🚨 START HERE

Cheat Sheet: Quick Middleware Debugging

Commands and fixes for the three most common middleware problems in production
🟡

req.body undefined

Immediate ActionCheck middleware order — body parser must come before routes
Commands
app.use(express.json()); // must be before app.use(routes)
console.log('req.body:', req.body); // add inside route handler
Fix NowMove app.use(express.json()) to the very top of your middleware stack
🟡

Request hangs with no response

Immediate ActionAdd a global middleware that logs every request passing through
Commands
app.use((req, res, next) => { console.log('Passing through:', req.path); next(); });
Check each middleware for missing next() — especially in conditional branches
Fix NowEnsure every middleware calls next() or sends a response (res.json(), res.send()) on all code paths
🟡

Error handler not catching errors

Immediate ActionVerify error handler has 4 parameters and is registered last
Commands
app.use((err, req, res, next) => { console.error(err); res.status(500).json({ error: err.message }); });
Check async routes: use try/catch and next(err) — Express 4 doesn't catch promise rejections
Fix NowInstall express-async-errors package to automatically catch async errors
Production Incident

The Silent Hanging Request: A Production Outage Caused by Missing next()

A team deployed an update to their Express API and noticed that about 5% of requests to a specific endpoint were timing out after 30 seconds. The app logs showed no errors — just incomplete transactions. Here's what happened.
SymptomPOST /orders endpoint intermittently hangs for 30 seconds before returning a 504 Gateway Timeout. No error in application logs, but some orders never completed. The issue correlated with high traffic (500+ req/s).
AssumptionThe team assumed the database query was slow under load. They optimised the database indexing, but the hanging persisted. They increased the database connection pool, but the issue remained.
Root causeA middleware function for audit logging was written with a conditional branch that forgot to call next() when the request already had an audit entry. Under specific conditions (when an x-audit-active header was present), the middleware returned early without calling next() and without sending a response. The request stayed open until the default timeout kicked in.
FixAdd an else branch that always calls next(). The corrected middleware now has a single exit path that guarantees next() is called unless a response is sent. Added a linter rule to enforce that every middleware must call next() or return a response.
Key Lesson
Every middleware must be exhaustive in its control flow: every branching path must either call next() or send a response. No exceptions.Use a linter plugin like eslint-plugin-express to flag missing next() calls in middleware functions.Set an aggressive timeout (e.g., 5 seconds) in production and monitor timeout errors as a leading indicator of hanging middleware.
Production Debug Guide

Common scenarios and the exact steps to diagnose them

req.body is undefined in a POST/PUT route handler1. Check that express.json() middleware is registered BEFORE the route definition. 2. Verify the request Content-Type is application/json. 3. If using body-parser separately, ensure it's imported and used correctly.
Client request hangs with no response (timeout after 30 seconds)1. Identify which middleware is the last to run before the hang — add a log before each next() call in suspect middleware. 2. Look for conditional returns that skip next() without sending a response. 3. Use app.use((req, res, next) => { console.log('Passing through'); next(); }) as a trace middleware to isolate the block.
Error handler never triggers, even when next(err) is called1. Count the parameters of your error-handling middleware — must have exactly 4: (err, req, res, next). 2. Ensure the error handler is registered LAST, after all app.use() and routes. 3. Verify that async errors are caught and passed to next(err) — Express 4 doesn't catch thrown promise rejections.

Every production Node.js API you've ever hit — whether it's logging your request, checking your auth token, or parsing the JSON body you sent — runs that logic through middleware. It's not a nice-to-have feature; it's the backbone of how Express applications are structured. Skip understanding middleware properly and you'll spend hours debugging why your route can't read req.body, or why your auth check fires on every route except the one that matters.

What Middleware Actually Is — The Anatomy of a Middleware Function

A middleware function in Express has a specific signature: it receives three arguments — req (the incoming request), res (the outgoing response), and next (a function that hands control to the next middleware in the chain). That third argument, next, is the key that separates middleware from a regular route handler. If you don't call next(), the request just hangs there — the client waits forever and nothing happens downstream.

Express processes middleware in the exact order you register it with app.use(). This isn't alphabetical, it isn't by route priority — it's purely sequential, top to bottom in your file. That means the order you write app.use() calls is a critical architectural decision, not just style.

There are five flavours of middleware you'll use in practice: application-level (app.use), router-level (router.use), error-handling (four arguments: err, req, res, next), built-in (like express.json()), and third-party (like morgan or helmet). Understanding which flavour to reach for — and when — separates junior from senior Express developers.

basicMiddlewarePipeline.js · JAVASCRIPT
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
const express = require('express');
const app = express();

// ─── Middleware 1: Request Logger ───────────────────────────────────────────
// Runs on EVERY incoming request regardless of route
app.use((req, res, next) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] Incoming: ${req.method} ${req.url}`);

  // Attach a custom property to req so later middleware/routes can use it
  req.requestTime = timestamp;

  // CRITICAL: call next() or the request dies here
  next();
});

// ─── Middleware 2: Parse JSON request bodies ─────────────────────────────────
// Without this, req.body is undefined for POST/PUT requests
app.use(express.json());

// ─── Middleware 3: Simple API Key Guard ─────────────────────────────────────
// Only runs if the two middleware above have already called next()
app.use((req, res, next) => {
  const apiKey = req.headers['x-api-key'];

  if (!apiKey || apiKey !== 'my-secret-key-123') {
    // Short-circuit: respond directly, do NOT call next()
    return res.status(401).json({
      error: 'Unauthorized',
      message: 'A valid x-api-key header is required'
    });
  }

  next(); // Key is valid — pass control to the route handler
});

// ─── Route Handler: only reached if all middleware above called next() ────────
app.get('/products', (req, res) => {
  res.json({
    message: 'Here are your products!',
    requestReceivedAt: req.requestTime // set by our logger middleware above
  });
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));
▶ Output
[2024-07-15T10:23:01.452Z] Incoming: GET /products
// If x-api-key header is missing:
{ "error": "Unauthorized", "message": "A valid x-api-key header is required" }

// If x-api-key: my-secret-key-123 is present:
{ "message": "Here are your products!", "requestReceivedAt": "2024-07-15T10:23:01.452Z" }
⚠ Watch Out: The Hanging Request
If your middleware function neither calls next() nor sends a response (res.json, res.send, etc.), the client's request will hang until it times out. This is one of the most common silent bugs in Express apps. Every middleware function must do one of two things: call next() or end the response.
📊 Production Insight
In production, hanging middleware often goes unnoticed until a user reports a timeout. A single middleware that fails to call next() under a specific condition can bring down a whole endpoint. Always audit middleware for exhaustive next() calls — especially in conditional branches, early returns, or error handling.
🎯 Key Takeaway
Middleware is a sequential pipeline where each function must call next() or send a response.
Failing to do either creates a silent hanging request, the most common Express bug in production.
Always ensure every code path in your middleware either calls next() or terminates the response.

Order Is Everything — Why Middleware Sequence Is an Architectural Decision

Here's a scenario that trips up almost every developer learning Express: you add express.json() after your route definitions and suddenly req.body is undefined. Or you put your authentication middleware after the route it's supposed to protect. Both are the same root problem — you misunderstood that Express is a sequential pipeline, not a declarative config system.

Think of it as a waterfall. Water only flows downward. A request enters at the top, hits each app.use() in the order it was registered, and continues down until something sends a response. The moment any middleware calls res.send() or res.json() without also calling next(), the waterfall stops — everything below that point is ignored.

This has real architectural implications. Your logging middleware should always be first — you want to log even failed requests. Your body-parsing middleware (express.json()) must come before any route that reads req.body. Your authentication middleware must come before the routes it protects, but after body parsing (because auth might need to read a token from the request body). Error-handling middleware — the special four-argument version — must always be last.

middlewareOrderDemo.js · JAVASCRIPT
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
const express = require('express');
const app = express();

// ✅ CORRECT ORDER — this is the pattern used in production apps

// STEP 1: Logging first — captures every request including failures
app.use((req, res, next) => {
  console.log(`LOG: ${req.method} ${req.path} at ${Date.now()}ms`);
  next();
});

// STEP 2: Body parsing before any route that needs req.body
app.use(express.json());
app.use(express.urlencoded({ extended: true })); // for HTML form submissions

// STEP 3: Authentication — after body parsing, before protected routes
const authenticateUser = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1]; // Bearer <token>

  if (!token) {
    return res.status(401).json({ error: 'No auth token provided' });
  }

  // In real apps you'd verify a JWT here. We'll simulate it.
  req.currentUser = { id: 42, name: 'Alex', role: 'admin' };
  next();
};

// STEP 4: Mount routes — these only run after the above middleware
app.post('/orders', authenticateUser, (req, res) => {
  // req.body is available because express.json() ran first
  // req.currentUser is available because authenticateUser ran
  const { productId, quantity } = req.body;

  res.status(201).json({
    message: `Order created by ${req.currentUser.name}`,
    order: { productId, quantity }
  });
});

// STEP 5: Error handler MUST be last and MUST have 4 parameters
// Express identifies error middleware by the 4-argument signature
app.use((err, req, res, next) => {
  console.error('Unhandled error:', err.message);
  res.status(500).json({ error: 'Something went wrong on our end' });
});

app.listen(3000);
▶ Output
// POST /orders with Authorization: Bearer mytoken123
// Body: { "productId": "SKU-99", "quantity": 3 }

LOG: POST /orders at 1721036581000ms
{
"message": "Order created by Alex",
"order": { "productId": "SKU-99", "quantity": 3 }
}
💡Pro Tip: Route-Specific vs Global Middleware
You don't have to apply middleware globally with app.use(). Pass it directly as a second argument to a specific route: app.get('/admin', authenticateUser, adminHandler). This gives you surgical control — your public /health check route won't hit your auth middleware at all, which is both more performant and more correct.
📊 Production Insight
Misordering middleware is a classic production incident. One team placed the CORS middleware after the routes, causing all preflight OPTIONS requests to fail. The fix: move CORS middleware to the very top of the stack. Another team put the rate-limiter after authentication, making the auth endpoint vulnerable to brute force. Rule: security middleware must always run before the protected logic.
🎯 Key Takeaway
Order determines what data and security is available at each step.
Body parser before routes, auth before protected endpoints, error handler last.
Write middleware registration like a waterfall — not a random list.

Writing Real-World Custom Middleware — Patterns You'll Actually Ship

There are three middleware patterns you'll reach for constantly in production: the guard (blocks requests that don't meet a condition), the enricher (attaches data to req for downstream handlers), and the transformer (modifies the response before it's sent). Let's build all three in a realistic context.

The enricher pattern is particularly powerful and underused. Instead of fetching a user from the database inside every single route handler, you write one middleware that fetches the user and attaches them to req. Every route downstream gets req.currentUser for free. Single responsibility, no code duplication.

The transformer pattern — often used with res.json() overriding — lets you wrap all API responses in a consistent envelope ({ success: true, data: ... }) without touching each route handler. This is how large teams enforce a consistent API contract across hundreds of endpoints written by dozens of developers.

The guard pattern is your security layer. Rate limiting, IP blocking, role-based access control — these all live in guard middleware that either calls next() or terminates the request with an appropriate HTTP status code.

realWorldMiddlewarePatterns.js · JAVASCRIPT
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
const express = require('express');
const app = express();
app.use(express.json());

// ─── PATTERN 1: The Enricher ─────────────────────────────────────────────────
// Simulates a DB lookup and attaches the user to every request
const attachUserToRequest = async (req, res, next) => {
  const userId = req.headers['x-user-id'];

  if (!userId) {
    return next(); // No user ID? Fine — downstream routes can handle it
  }

  try {
    // Simulate async database call
    const fakeDbUser = await Promise.resolve({
      id: userId,
      name: 'Jordan Lee',
      role: 'editor',
      plan: 'pro'
    });

    req.currentUser = fakeDbUser; // Attach once, use everywhere
    next();
  } catch (dbError) {
    next(dbError); // Pass errors to the error-handling middleware
  }
};

// ─── PATTERN 2: The Guard ────────────────────────────────────────────────────
// Role-based access control — only 'admin' users can proceed
const requireAdminRole = (req, res, next) => {
  if (!req.currentUser) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  if (req.currentUser.role !== 'admin') {
    return res.status(403).json({
      error: 'Forbidden',
      message: `Your role '${req.currentUser.role}' cannot access this resource`
    });
  }

  next();
};

// ─── PATTERN 3: The Transformer ──────────────────────────────────────────────
// Wraps ALL JSON responses in a consistent { success, data } envelope
const responseEnvelope = (req, res, next) => {
  const originalJson = res.json.bind(res); // Save the original method

  res.json = (payload) => {
    // If the payload already has an 'error' key, don't wrap it
    if (payload && payload.error) {
      return originalJson(payload);
    }
    return originalJson({ success: true, data: payload });
  };

  next();
};

// ─── Wire everything together ────────────────────────────────────────────────
app.use(attachUserToRequest); // Runs globally — enriches req for all routes
app.use(responseEnvelope);    // Runs globally — wraps all success responses

// Public route — no guard needed
app.get('/articles', (req, res) => {
  const greeting = req.currentUser
    ? `Hello ${req.currentUser.name}!`
    : 'Hello, guest!';

  res.json({ articles: ['Intro to Node', 'Express Deep Dive'], greeting });
});

// Protected route — guard middleware applied only here
app.delete('/articles/:id', requireAdminRole, (req, res) => {
  res.json({ deleted: true, articleId: req.params.id });
});

// Error handler (always last)
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(3000, () => console.log('Running on port 3000'));
▶ Output
// GET /articles with header x-user-id: 42
{ "success": true, "data": { "articles": ["Intro to Node", "Express Deep Dive"], "greeting": "Hello Jordan Lee!" } }

// DELETE /articles/7 with x-user-id: 42 (role: 'editor', not 'admin')
{ "error": "Forbidden", "message": "Your role 'editor' cannot access this resource" }

// DELETE /articles/7 with x-user-id: 42 (role changed to 'admin')
{ "success": true, "data": { "deleted": true, "articleId": "7" } }
🔥Interview Gold: Passing Data Between Middleware
The req object is shared across the entire middleware chain for a single request. This makes it the perfect place to attach data that multiple middleware or route handlers need — like req.currentUser or req.startTime. Never use a module-level variable to share per-request data; that causes race conditions under concurrent load.
📊 Production Insight
The transformer pattern is powerful but risky — overriding res.json affects all responses, including error responses. Always check if the payload already has an error property to avoid double-wrapping. In production, one team forgot this check and error responses came back nested like { success: true, data: { success: true, data: { error: ... } } }. Debugging that was a nightmare.
🎯 Key Takeaway
Custom middleware patterns: guard (block decision), enricher (attach data to req), transformer (modify response).
Use enricher to avoid repeated DB calls in every route.
Override res.json carefully — always respect existing error structures.

Middleware for Input Validation and Data Sanitization

One of the most common middleware patterns you'll write is input validation. Instead of validating request bodies inside every route handler, you create a reusable middleware that checks required fields, data types, and constraints — and returns a 400 error immediately if something's off. This keeps route handlers clean and prevents bad data from reaching your business logic.

A well-designed validation middleware uses a schema or a set of rules. You can pass the validation rules as arguments to a middleware factory function. That way, each route gets its own validation rules, but the validation logic is written once. This pattern is a major reason why validation libraries like Joi, express-validator, or Zod are so popular in the Node.js ecosystem.

Validation middleware should run after body parsing (so req.body exists) and before route handlers (to short-circuit invalid requests). It should also handle nested objects, array items, and optional fields with default values. In production, express-validator is a battle-tested choice because it integrates directly with Express and provides chainable validation rules.

validationMiddleware.js · JAVASCRIPT
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
const express = require('express');
const app = express();
app.use(express.json());

// ─── Validation Middleware Factory ───────────────────────────────────────────
// Returns a middleware that validates req.body against a schema object
function validate(schema) {
  return (req, res, next) => {
    const errors = [];
    
    for (const [field, rules] of Object.entries(schema)) {
      const value = req.body[field];
      
      if (rules.required && (value === undefined || value === null)) {
        errors.push(`${field} is required`);
        continue;
      }
      
      if (value === undefined) continue; // optional field, skip further checks
      
      if (rules.type && typeof value !== rules.type) {
        errors.push(`${field} must be of type ${rules.type}`);
      }
      
      if (rules.minLength && typeof value === 'string' && value.length < rules.minLength) {
        errors.push(`${field} must be at least ${rules.minLength} characters`);
      }
      
      if (rules.maxLength && typeof value === 'string' && value.length > rules.maxLength) {
        errors.push(`${field} must be at most ${rules.maxLength} characters`);
      }
      
      if (rules.pattern && typeof value === 'string' && !rules.pattern.test(value)) {
        errors.push(`${field} does not match required pattern`);
      }
    }
    
    if (errors.length > 0) {
      return res.status(400).json({ error: 'Validation failed', details: errors });
    }
    
    next();
  };
}

// ─── Define validation schemas per route ─────────────────────────────────────
const createUserSchema = {
  name: { required: true, type: 'string', minLength: 2, maxLength: 100 },
  email: { required: true, type: 'string', pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
  age: { required: false, type: 'number' }
};

// ─── Route with validation middleware ───────────────────────────────────────
app.post('/users', validate(createUserSchema), (req, res) => {
  // At this point, req.body is guaranteed to be valid
  const { name, email, age } = req.body;
  res.status(201).json({ message: `User ${name} created`, email });
});

// ─── Error handler (last) ───────────────────────────────────────────────────
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(3000);
▶ Output
// POST /users with body: { "name": "Al", "email": "bad" }
{ "error": "Validation failed", "details": ["name must be at least 2 characters", "email does not match required pattern"] }

// POST /users with body: { "name": "Alice", "email": "alice@example.com" }
{ "message": "User Alice created", "email": "alice@example.com" }

// POST /users with body: {}
{ "error": "Validation failed", "details": ["name is required", "email is required"] }
Mental Model
Mental Model: Validation as an Early Exit
Think of validation middleware as a bouncer at a club — it checks IDs before letting anyone in. The bouncer doesn't serve drinks; it just ensures only valid guests proceed.
  • Validation middleware runs after body parsing, before business logic.
  • It short-circuits invalid requests with a 400 response — no wasted DB calls.
  • Use a factory function to keep validation reusable across routes.
  • Combine with a schema library (Joi, Zod) for complex validations.
📊 Production Insight
In production, validation middleware prevents entire classes of bugs. A team once shipped an endpoint that accepted arbitrary MongoDB query strings in the request body, leading to a NoSQL injection vulnerability. Adding a validation middleware that rejected non-string fields blocked the attack. Rule: validation isn't just for user experience — it's a security boundary.
🎯 Key Takeaway
Validation middleware moves check logic out of route handlers for cleaner code.
Use a factory pattern to define per-route schemas without repeating the validation logic.
Validation is a security boundary — always validate at the API gateway or middleware layer.

Error-Handling Middleware — The Safety Net Every Express App Needs

Express has a special type of middleware dedicated to error handling, and it's identified by one thing alone: having exactly four parameters (err, req, res, next). Even if you never use next inside it, you must declare it — otherwise Express treats it as regular middleware and your errors fall into a black hole.

The way you trigger error-handling middleware is by calling next(error) with any argument inside a regular middleware or route. The moment Express sees next called with an argument, it skips all remaining regular middleware and jumps straight to the nearest error-handling middleware.

In async route handlers, errors thrown in try/catch must be explicitly forwarded with next(err). But with Express 5 (currently in release candidate), async errors are caught automatically. For Express 4 — which the vast majority of production apps still run — you need to either wrap async functions yourself or use a helper like express-async-errors.

A mature Express app typically has layered error handlers: one for known operational errors (validation failures, not-found resources) and one final catch-all for unexpected programmer errors.

errorHandlingMiddleware.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
const express = require('express');
const app = express();
app.use(express.json());

// ─── Custom Error Class ──────────────────────────────────────────────────────
// Create typed errors so your error handler can respond intelligently
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true; // Distinguishes expected errors from bugs
  }
}

// ─── Route that deliberately throws a known error ────────────────────────────
app.get('/users/:id', async (req, res, next) => {
  try {
    const userId = parseInt(req.params.id, 10);

    if (isNaN(userId) || userId <= 0) {
      // Throw a known operational error — not a bug, just bad input
      throw new AppError('User ID must be a positive integer', 400);
    }

    // Simulate: user not found in database
    if (userId > 100) {
      throw new AppError(`No user found with ID ${userId}`, 404);
    }

    res.json({ id: userId, name: 'Sam Rivera', email: 'sam@example.com' });

  } catch (err) {
    // Forward ALL errors to the error-handling middleware below
    next(err);
  }
});

// ─── 404 Handler — catches requests for routes that don't exist ──────────────
// Must come AFTER all your real routes
app.use((req, res, next) => {
  next(new AppError(`Route ${req.method} ${req.path} not found`, 404));
});

// ─── Global Error Handler — MUST have 4 parameters ──────────────────────────
// MUST be registered last, after all routes and other middleware
app.use((err, req, res, next) => {
  // Log everything — even handled errors are worth knowing about
  console.error(`[ERROR] ${err.message}`, {
    statusCode: err.statusCode,
    path: req.path,
    stack: err.isOperational ? undefined : err.stack
  });

  // Operational errors: send specific, safe message to client
  if (err.isOperational) {
    return res.status(err.statusCode).json({
      error: err.message
    });
  }

  // Programmer errors (bugs): never leak stack traces to clients
  res.status(500).json({
    error: 'An unexpected error occurred. Our team has been notified.'
  });
});

app.listen(3000);
▶ Output
// GET /users/abc
{ "error": "User ID must be a positive integer" } // 400 Bad Request

// GET /users/999
{ "error": "No user found with ID 999" } // 404 Not Found

// GET /users/42
{ "id": 42, "name": "Sam Rivera", "email": "sam@example.com" } // 200 OK

// GET /this-route-does-not-exist
{ "error": "Route GET /this-route-does-not-exist not found" } // 404
⚠ Watch Out: Async Errors in Express 4 Are Silent
In Express 4, if an async function throws and you don't catch it and call next(err), Express never sees the error. The request hangs or crashes the process. Always wrap async route handlers in try/catch and forward errors with next(err). Alternatively, install the express-async-errors package which monkey-patches Express to handle this automatically.
📊 Production Insight
A staggering number of production crashes come from unhandled promise rejections in Express 4. One infamous outage at a major food delivery service occurred because an async route handler threw an error that wasn't caught — the Node.js process crashed, taking down all active connections. The fix: global process.on('unhandledRejection') handler plus express-async-errors. Rule: never assume async errors bubble up to Express's error handler — they don't in Express 4.
🎯 Key Takeaway
Error-handling middleware is identified by its 4-parameter signature and must be registered last.
Always forward async errors with next(err) in Express 4 — unhandled rejections crash the process.
Use custom error classes to distinguish operational errors (bad input) from programmer bugs (null reference).
🗂 Application-Level vs Router-Level Middleware
When to use each and how they affect request processing
AspectApplication-Level Middleware (app.use)Router-Level Middleware (router.use)
ScopeApplies to every request hitting the entire appApplies only to routes mounted on that specific router
Typical use caseLogging, body parsing, global auth, CORS headersFeature-specific auth, resource-specific rate limiting
Registrationapp.use(middlewareFn)const router = express.Router(); router.use(middlewareFn)
ModularityLower — tightly coupled to the main app fileHigher — encapsulated within a feature module
Performance impactEvery request pays the costOnly requests matched by router prefix pay the cost
Best forCross-cutting concerns (logging, security headers)Domain-specific concerns (admin routes, API versioning)

🎯 Key Takeaways

  • Middleware is a sequential pipeline — Express processes every app.use() call in the order it was registered, top to bottom. Order isn't a style choice; it's logic.
  • Every middleware function must either call next() to continue the chain or send a response to terminate it — doing neither causes the client request to hang silently.
  • Error-handling middleware is identified solely by its four-parameter signature (err, req, res, next). Register it last in your file, after all routes and regular middleware.
  • The req object is the shared data bus for a single request's journey through the pipeline. Attach per-request context (like the authenticated user) to req in middleware, and every downstream handler gets it for free.
  • Validation middleware is a security boundary — run it after body parsing and before route handlers to prevent bad input from reaching your business logic.

⚠ Common Mistakes to Avoid

    Forgetting to call next() in middleware
    Symptom

    The client request hangs indefinitely with no response and no error in the console. The connection eventually times out after 30 seconds, but the server logs show nothing useful.

    Fix

    Every middleware must either call next() to continue the chain, or send a response using res.json()/res.send(). Add a linter rule or code review checklist item specifically for this.

    Registering express.json() after the routes that need req.body
    Symptom

    req.body is undefined inside your POST/PUT route handlers even though you're sending a JSON body with correct Content-Type.

    Fix

    Always place app.use(express.json()) before any route definitions in your file. A simple rule: body parsers go at the top, routes go at the bottom.

    Writing an error-handling middleware with only 3 parameters
    Symptom

    Express treats it as regular middleware, so errors passed via next(err) are silently ignored and the client either hangs or gets an unhandled rejection crash.

    Fix

    Error-handling middleware must always declare exactly four parameters: (err, req, res, next). Even if you never use next, include it. Express uses the function's arity (parameter count) to identify error handlers.

    Using sync middleware for expensive async operations without error handling
    Symptom

    An async database call in middleware throws an error, but it's not caught. The request hangs or the process crashes because the promise rejection is unhandled.

    Fix

    Wrap async code in try/catch and forward errors via next(err). Alternatively, use express-async-errors to automatically catch async errors.

Interview Questions on This Topic

  • QWhat is the difference between app.use() and app.get() in Express, and how does each interact with the middleware pipeline?JuniorReveal
    Both are middleware registration methods. app.use() mounts middleware for all HTTP methods and matches any path that starts with the given path (default '/'). app.get() (and app.post(), etc.) mounts middleware only for a specific HTTP method and exact path. The pipeline processes all registered middleware in order, regardless of method, but only app.get() and similar will match specific routes. app.use() is typically used for global middleware (logging, CORS, parsing).
  • QHow does Express identify an error-handling middleware function differently from a regular middleware function, and what happens if you accidentally omit the first parameter?Mid-levelReveal
    Express identifies error-handling middleware by its function signature — specifically, the number of parameters. A function with exactly four parameters (err, req, res, next) is considered error-handling. If you omit the err parameter (leaving 3), Express treats it as regular middleware. When an error is passed via next(err), Express skips all regular middleware and looks for the next error-handling middleware. If none is found, the error becomes unhandled and may crash the process. Always include all four parameters, even if you don't use next.
  • QIf you have async middleware that fetches data from a database, and that database call throws an error, how do you ensure Express's error-handling middleware actually receives and processes that error in Express 4?SeniorReveal
    In Express 4, async errors are not automatically caught. You must wrap the async code in a try/catch block and call next(err) inside the catch. For example: ``javascript app.use(async (req, res, next) => { try { const user = await db.findUser(); req.user = user; next(); } catch (err) { next(err); } }); ` Alternatively, install the express-async-errors` package, which patches Express to automatically catch promise rejections. Without this, unhandled promise rejections will silently crash the Node.js process.

Frequently Asked Questions

What is middleware in Express.js and what does it do?

Middleware in Express.js is a function that runs during the lifecycle of an HTTP request, between the request arriving at the server and the response being sent to the client. Each middleware function receives the request object, the response object, and a next() function. It can read or modify the request, terminate the request/response cycle, or pass control to the next middleware in the chain by calling next().

Why is the order of middleware important in Express.js?

Express processes middleware strictly in the order it's registered using app.use(). If you register express.json() after your route handlers, those routes will receive an undefined req.body because the body parser never ran before them. Similarly, if an authentication middleware is registered after the routes it's meant to protect, those routes are left unguarded. Order determines what data and behaviour is available at each step.

What's the difference between middleware and a route handler in Express?

The core difference is intent and behaviour within the pipeline. A route handler is a middleware that is expected to end the request/response cycle by sending a response. A middleware function is expected to do some work and then call next() to pass control forward. In practice, Express doesn't distinguish them technically — both have the same (req, res, next) signature — but conceptually, middleware is for cross-cutting concerns (auth, logging, parsing) while route handlers are for business logic.

How do I handle async errors in Express 4 middleware?

Wrap the async code in a try/catch block and call next(err) in the catch. Express 4 does not catch promise rejections automatically. Without proper handling, unhandled rejections will crash the Node.js process. You can also install the express-async-errors package to automatically patch Express 4 to catch async errors.

Can I use middleware only for specific routes?

Yes, you can pass middleware as a second argument to route definitions, e.g., app.get('/admin', isAdmin, handler). This applies only to that specific route and method. You can also use router-level middleware with express.Router() to scope middleware to a group of routes (like /api/v1/*).

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousREST API with Express.jsNext →Node.js with MongoDB
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged