Managing Request Flow with Express Middleware
Stop the 'hanging request' problem in Express.js. Learn how to implement global and route-level middleware, handle errors correctly, and pass data through the request pipeline.
28 Sept 2026, 08:32 UTC

The Hanging Request Problem
One of the most common frustrations when building an Express.js application is the "hanging request." You send a request to your server, the browser spinner spins indefinitely, and the server logs show nothing. This almost always happens because a middleware function failed to signal the end of its operation.
In Express, middleware functions are the glue between the raw incoming request and your final route handler. If a middleware doesn't call next() to pass control to the next function, or res.send() to terminate the cycle, the request remains in limbo. The takeaway: every middleware must either hand off the baton or finish the race.
The Middleware Pipeline
Express processes requests in a deterministic sequence. When a request hits your server, it passes through a stack of functions in the exact order they were defined using app.use() or route-specific arguments. This allows you to separate cross-cutting concerns—logic that applies to many routes, such as authentication, logging, or input validation—from your actual business logic.
Global vs. Route-Level Application
Global middleware is applied using app.use() and runs for every single request entering the application. Route-level middleware is passed as an argument to a specific method (like app.get), ensuring that expensive checks, such as database-backed session validation, only run where they are actually needed.
Practical Implementation: Request Logging and Auth
To implement custom middleware, you define a function with access to the request (req), response (res), and the next callback. A common pattern is to attach data to the req object so that subsequent handlers can use it without re-calculating it.
const express = require('express');
const app = express();
// 1. Global Logger Middleware
// Runs for every request
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // Pass control to the next handler
});
// 2. Route-Specific Auth Middleware
// Only runs for protected routes
const authenticate = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (apiKey === 'secret-123') {
req.user = { id: 1, name: 'Admin' }; // Attach data for later use
next();
} else {
res.status(401).send('Unauthorized'); // Terminate request here
}
};
// Public route
app.get('/', (req, res) => {
res.send('Welcome to the public home page!');
});
// Protected route using the authenticate middleware
app.get('/dashboard', authenticate, (req, res) => {
res.send(`Hello ${req.user.name}, welcome to your dashboard.`);
});
app.listen(3000);
Running the Example
- Environment: Node.js environment with
expressinstalled. - Execution: Run the script with
node server.js. - Verification:
- Request
GET /: Should return 200 and log the request. - Request
GET /dashboardwithout header: Should return 401. - Request
GET /dashboardwith headerx-api-key: secret-123: Should return 200 and the greeting.
- Request
Handling Errors Correctly
Standard middleware cannot catch errors thrown in previous handlers unless you use a specific Error-Handling Middleware. Express identifies these functions by the number of arguments they accept. While standard middleware takes three (req, res, next), error handlers must take four: (err, req, res, next).
Critical Placement: Error handlers must be defined after all other app.use() and route calls. If placed at the top of the file, they will be ignored because the error hasn't occurred yet in the pipeline.
// This must be the LAST app.use() in your file
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
Trade-offs and Limitations
While modifying the req object (e.g., req.user = ...) is the standard way to pass data forward, it introduces a risk of "property pollution." In large projects, it becomes difficult to track which middleware added which property, potentially leading to runtime errors if a route handler expects a property that a skipped middleware didn't provide.
Additionally, avoid performing heavy synchronous computation (like large JSON parsing or complex loops) inside middleware. Because Node.js is single-threaded, a blocking operation in a global middleware will freeze the server for every other concurrent user.
Closing Action
To audit your current Express app for middleware bottlenecks or hanging requests, search your codebase for app.use and next(). Ensure every conditional branch in your middleware either calls next() or sends a response. If you are seeing unexpected 500 errors, verify that your error-handling middleware is the final definition in your application stack.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.