Fix: Express Middleware Not Working — Order Wrong, Errors Not Caught, or async Errors Silently Dropped
Quick Answer
How to fix Express middleware issues — middleware execution order, error-handling middleware signature, async error propagation with next(err), and common middleware misconfigurations.
The Problem
Middleware runs but doesn’t affect the response, or it’s skipped entirely:
app.use((req, res, next) => {
req.user = getUser(req); // Sets req.user
next();
});
app.get('/profile', (req, res) => {
console.log(req.user); // undefined — middleware didn't run?
});Or an error handler never fires even when errors are thrown:
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
app.get('/data', async (req, res) => {
const data = await db.query(); // throws — but error handler never runs
res.json(data);
});Or a specific route middleware is applied to the wrong routes:
app.use('/admin', authMiddleware); // Should protect admin routes
app.get('/admin/dashboard', (req, res) => {
res.send('Dashboard'); // Accessible without auth
});Why This Happens
Express middleware executes in the order it’s registered. Most issues trace back to these rules:
- Order matters absolutely — middleware registered after a route handler doesn’t run for that route. Express processes middleware top-to-bottom, stopping when a response is sent.
- Error-handling middleware needs exactly 4 arguments —
(err, req, res, next). If you write(err, req, res)(omittingnext), Express treats it as regular middleware, not an error handler, and skips it. asyncfunctions don’t forward errors automatically — if anasyncroute throws, Express doesn’t catch it unless you explicitly callnext(err)or wrap the handler. Uncaught promise rejections are silently dropped in older Express versions.next()must be called — if a middleware doesn’t callnext()and doesn’t send a response, the request hangs forever.
Fix 1: Register Middleware Before the Routes It Should Affect
Express runs middleware in registration order. Middleware must appear before the routes it needs to intercept:
const express = require('express');
const app = express();
// WRONG — middleware registered after the route
app.get('/profile', (req, res) => {
console.log(req.user); // undefined, middleware hasn't run yet
res.json({ user: req.user });
});
app.use((req, res, next) => { // Too late — /profile already matched
req.user = { id: 1, name: 'Alice' };
next();
});
// CORRECT — middleware registered before the route
app.use((req, res, next) => {
req.user = { id: 1, name: 'Alice' };
next();
});
app.get('/profile', (req, res) => {
console.log(req.user); // { id: 1, name: 'Alice' }
res.json({ user: req.user });
});Standard middleware registration order:
const app = express();
// 1. Built-in / third-party middleware first
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());
app.use(helmet());
// 2. Logging
app.use(morgan('dev'));
// 3. Auth / session middleware
app.use(sessionMiddleware);
app.use(authMiddleware);
// 4. Route handlers
app.use('/api/users', userRouter);
app.use('/api/orders', orderRouter);
// 5. 404 handler — after all routes
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
// 6. Error handler — must be last
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message });
});Fix 2: Use the Correct Error-Handler Signature
Express identifies error-handling middleware by its 4-argument signature. All four parameters must be declared, even if you don’t use next:
// WRONG — 3 arguments, Express treats this as regular middleware
app.use((err, req, res) => {
res.status(500).send(err.message); // Never called for errors
});
// WRONG — named function but same problem
app.use(function errorHandler(err, req, res) {
res.status(500).send(err.message);
});
// CORRECT — 4 arguments, Express recognizes this as an error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
});Error handler must be registered last:
// All routes and middleware first
app.use('/api', apiRouter);
// Error handler at the bottom — after all routes
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err); // Delegate to default handler if response started
}
res.status(err.status || 500).json({ error: err.message });
});Note:
res.headersSentguards against writing to a response that’s already been started. Attempting to set headers afterres.send()throws aCannot set headers after they are senterror.
Fix 3: Propagate async Errors to next()
Express 4 doesn’t catch rejected promises in route handlers. You must forward async errors explicitly:
// WRONG — thrown error silently drops in Express 4
app.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users'); // throws — never caught
res.json(users);
});
// CORRECT — catch and forward to next()
app.get('/users', async (req, res, next) => {
try {
const users = await db.query('SELECT * FROM users');
res.json(users);
} catch (err) {
next(err); // Forwards to the error-handling middleware
}
});Wrap async handlers to avoid repetition:
// asyncHandler wrapper — eliminates try/catch boilerplate
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Usage — clean, no try/catch needed
app.get('/users', asyncHandler(async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
}));
app.post('/users', asyncHandler(async (req, res) => {
const user = await db.create(req.body);
res.status(201).json(user);
}));Note: Express 5 (currently in beta) handles async errors automatically — rejected promises in route handlers are automatically forwarded to the next error handler. If you’re on Express 5, the
asyncHandlerwrapper is unnecessary.
Check your Express version:
npm list expressIf you see [email protected], async error forwarding is built in.
Fix 4: Apply Route-Specific Middleware Correctly
Middleware can be scoped to specific routes or passed inline as route-level middleware:
// WRONG — this mounts authMiddleware at /admin prefix
// but doesn't protect sub-routes unless the router handles it
app.use('/admin', authMiddleware);
app.get('/admin/dashboard', (req, res) => {
// authMiddleware ran, but only if it calls next() correctly
});
// CORRECT — inline middleware for specific routes
app.get('/admin/dashboard', authMiddleware, (req, res) => {
res.send('Dashboard');
});
// CORRECT — apply to a router, protecting all its routes
const adminRouter = express.Router();
adminRouter.use(authMiddleware); // Applies to all routes in this router
adminRouter.get('/dashboard', (req, res) => res.send('Dashboard'));
adminRouter.get('/users', (req, res) => res.send('Users'));
adminRouter.get('/settings', (req, res) => res.send('Settings'));
app.use('/admin', adminRouter);Multiple inline middleware:
app.post('/upload',
authenticate, // Check JWT
authorize('admin'), // Check role
upload.single('file'), // multer file upload
validateFileType, // Custom validation
async (req, res) => {
// All middleware passed — process the upload
res.json({ url: req.file.path });
}
);Fix 5: Common Middleware Misconfigurations
express.json() not applied — body is undefined:
// WRONG — no body parser
app.post('/data', (req, res) => {
console.log(req.body); // undefined
});
// CORRECT
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/data', (req, res) => {
console.log(req.body); // { name: 'Alice', ... }
});CORS middleware applied too late:
// WRONG — CORS headers missing on preflight OPTIONS requests
app.use('/api', router);
app.use(cors()); // Too late
// CORRECT — CORS before routes
app.use(cors({
origin: process.env.ALLOWED_ORIGIN,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
app.use('/api', router);Middleware that accidentally ends the request chain:
// BUG — sends response AND calls next()
app.use((req, res, next) => {
if (!req.headers.authorization) {
res.status(401).json({ error: 'Unauthorized' });
// Missing return — execution continues, next() also runs
next(); // Causes "Cannot set headers after they are sent"
} else {
next();
}
});
// CORRECT — return after sending response
app.use((req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});Static files middleware ordering:
// WRONG — API routes after static files causes issues if paths overlap
app.use(express.static('public'));
app.use('/api', apiRouter);
// CORRECT — API routes first, static files as fallback
app.use('/api', apiRouter);
app.use(express.static('public'));Fix 6: Build a Production-Ready Middleware Stack
A complete, production-ready Express middleware configuration:
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const app = express();
// Security headers
app.use(helmet());
// CORS — configure before routes
app.use(cors({
origin: process.env.CORS_ORIGIN?.split(',') || 'http://localhost:3000',
credentials: true,
}));
// Rate limiting — apply before body parsing to reduce DoS risk
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api', limiter);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Logging
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
// Request ID — attach to all requests for tracing
app.use((req, res, next) => {
req.id = crypto.randomUUID();
res.setHeader('X-Request-Id', req.id);
next();
});
// Routes
app.use('/api/v1/users', usersRouter);
app.use('/api/v1/orders', ordersRouter);
app.use('/health', (req, res) => res.json({ status: 'ok' }));
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: `Cannot ${req.method} ${req.path}` });
});
// Centralized error handler
app.use((err, req, res, next) => {
// Log error with request context
console.error({
requestId: req.id,
method: req.method,
path: req.path,
error: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
});
if (res.headersSent) return next(err);
const status = err.status || err.statusCode || 500;
res.status(status).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
requestId: req.id,
});
});Creating reusable middleware with options:
// Middleware factory — accepts configuration
function requireRole(role) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (req.user.role !== role) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// Usage
app.get('/admin/users', authenticate, requireRole('admin'), listUsers);
app.get('/reports', authenticate, requireRole('manager'), getReports);Still Not Working?
Middleware runs but next() is never called — if a middleware sends a response (like res.json()) it must not call next(). And if it doesn’t send a response, it must call next(). Forgetting either causes requests to hang indefinitely. Always ensure one of these two things happens in every code path.
Router-level app.use() with a path prefix strips the prefix — when you mount a router with app.use('/api', router), the /api prefix is stripped before reaching the router. Inside the router, define routes as /users, not /api/users:
// router.js
router.get('/users', handler); // Correct — matches /api/users
// NOT
router.get('/api/users', handler); // Wrong — would match /api/api/usersMiddleware not running for OPTIONS requests — some authentication middleware returns early for non-GET/POST methods, accidentally blocking CORS preflight requests. Explicitly allow OPTIONS to pass through before auth checks:
app.use((req, res, next) => {
if (req.method === 'OPTIONS') return next(); // Let CORS handle it
authenticate(req, res, next);
});express-async-errors package as an alternative — instead of writing asyncHandler wrappers, install express-async-errors and require it once at the top of your app. It monkey-patches Express 4 to forward async errors automatically:
require('express-async-errors'); // Add this once, before route definitions
const express = require('express');
// Now async route errors are forwarded to the error handler automaticallyFor related Express issues, see Fix: Node.js Stream Error and Fix: Express Rate Limit Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Express req.body Is undefined
How to fix req.body being undefined in Express — missing body-parser middleware, wrong Content-Type header, middleware order issues, and multipart form data handling.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Kafka Consumer Not Receiving Messages, Connection Refused, and Rebalancing Errors
How to fix Apache Kafka issues — consumer not receiving messages, auto.offset.reset, Docker advertised.listeners, max.poll.interval.ms rebalancing, MessageSizeTooLargeException, and KafkaJS errors.
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.