Skip to content

Fix: Express Middleware Not Working — Order Wrong, Errors Not Caught, or async Errors Silently Dropped

FixDevs ·

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) (omitting next), Express treats it as regular middleware, not an error handler, and skips it.
  • async functions don’t forward errors automatically — if an async route throws, Express doesn’t catch it unless you explicitly call next(err) or wrap the handler. Uncaught promise rejections are silently dropped in older Express versions.
  • next() must be called — if a middleware doesn’t call next() 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.headersSent guards against writing to a response that’s already been started. Attempting to set headers after res.send() throws a Cannot set headers after they are sent error.

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 asyncHandler wrapper is unnecessary.

Check your Express version:

npm list express

If 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/users

Middleware 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 automatically

For related Express issues, see Fix: Node.js Stream Error and Fix: Express Rate Limit Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles