Backend Node.js Course · Level 2
Middleware Pattern in Express
On this page
1. Execution order and next()
- Middleware functions form a chain of request handlers that execute sequentially in their exact declaration order (
app.use(),app.get(path, ...middlewares, handler)). - A standard route handler accepts
(req, res). Middleware functions receive an additional third parameter:(req, res, next). - Calling
next()signals "task completed; advance to the next middleware/handler in the chain." If you forget to callnext(), Express halts execution right there, causing the request to hang (the client waits indefinitely because nores.send()orres.json()is ever invoked). - Verified through hands-on practice: omitting
next()insidelogMiddlewarecausescurlto hang with no response returned. Addingnext()allows the request to continue normally to the next middleware and then to the handler. - An authorization check middleware (such as validating an
x-api-keyheader), if invalid, responds withres.status(401).json({...})and deliberately does not callnext(), short-circuiting the request right there without reaching the route handler.
2. Error-handling middleware
- Express provides a specialized middleware variant exclusively for error handling that accepts 4 parameters:
(err, req, res, next), unlike regular middleware (2–3 parameters).
js
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong' });
});
- Express distinguishes between the two types by inspecting
fn.length(the declared parameter count of the function) upon registration viaapp.use(): exactly 4 parameters classifies it as error-handling middleware, while 2–3 parameters registers it as standard middleware. - Calling
next(err)with 1 argument (unlikenext()with no arguments) signals an error condition: Express skips all remaining standard middleware and route handlers, jumping directly to the nearest downstream error-handling middleware.
3. Express 4 does not catch errors from async handlers automatically
- Express 4 (widely used in legacy projects) does not automatically catch errors when an
asyncroute handler throws or rejects without atry/catchblock. In a handler likeasync (req, res) => { await getOrderFromDB(id); }, if the promise rejects, Express 4 remains completely unaware of the error and fails to forward it to error-handling middleware. - Consequence: the request hangs (identical to a missing
next()bug) because nores.send()is executed. This also serves as a practical example of the unhandled Promise rejection covered in Phase 1, which can crash the entire Node process on modern versions (Node 15+). - Starting only from Express 5 are errors thrown inside async handlers caught automatically and forwarded to error-handling middleware.
4. Workarounds for Express 4: try/catch + next(err), or the asyncHandler wrapper
- Manual approach per route:
js
app.get('/orders/:id', async (req, res, next) => {
try {
const order = await getOrderFromDB(req.params.id);
res.json(order);
} catch (err) {
next(err);
}
});
- Repeating
try/catchacross every route is boilerplate-heavy. A cleaner approach uses anasyncHandlerwrapper function that automatically catches rejections and invokesnext(err)on your behalf:
js
function asyncHandler(fn) {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
}
app.get('/orders/:id', asyncHandler(async (req, res) => {
const order = await getOrderFromDB(req.params.id);
res.json(order);
}));
.catch(next)is shorthand for.catch(err => next(err))when no intermediate processing is required before callingnext(passing the function reference directly, known as point-free style). Writing an explicit arrow function is only necessary when adding extra logic (such as logging withconsole.errorbefore invokingnext).
