Conditional Middleware in Express: Why runMiddleware Exists (and When You Should Remove It)

Introduction
In many real-world Express.js applications, especially authentication flows, developers encounter a subtle but important problem:
How do you execute an Express middleware conditionally inside a route handler and still keep the code readable and safe?
This blog explains:
Why
runMiddleware(req, res, middleware)is sometimes introducedWhat problem it actually solves
Why
res.headersSentchecks appear alongside itWhen this pattern is justified
When it is unnecessary and should be removed
The goal is not abstraction for its own sake, but clarity, correctness, and debuggability.
The Scenario
Consider a login endpoint that supports two actions:
Generate OTP (rate-limited)
Login using OTP (not rate-limited)
Both actions share the same route:
POST /admin
The OTP action must be protected by a rate limiter, but the login action should not be throttled. This makes route-level middleware insufficient.
Why You Cannot await Express Middleware Directly
Express middleware follows this signature:
(req, res, next) => {}
It is callback-based, not promise-based. Therefore, the following does not work:
await customLimiter(req, res);
There is no promise to await, and Express provides no built-in way to pause execution until next() is called.
The Purpose of runMiddleware
The runMiddleware helper is a Promise adapter. It converts a callback-style middleware into something that can be awaited.
function runMiddleware(req, res, fn) {
return new Promise((resolve, reject) => {
fn(req, res, (err) => {
if (err) return reject(err);
resolve();
});
});
}
This allows middleware to be executed inside a route handler, conditionally:
await runMiddleware(req, res, customLimiter);
At this point, execution pauses until the middleware either:
Calls
next()→ promise resolvesSends a response → route must stop
Why res.headersSent Is Mandatory
Rate limiters often terminate the request themselves:
res.status(429).json({ message: "Too many requests" });
When this happens:
The response is already sent
Any further attempt to write to
reswill crashDatabase connections must be released
That is why this guard exists:
if (res.headersSent) {
client.release();
return;
}
Without it, your application risks:
❌ "Cannot set headers after they are sent" errors
❌ Database connection leaks
❌ Unpredictable runtime failures
This check is not defensive coding—it is required for correctness.
When This Pattern Is Justified
Using runMiddleware is appropriate only when all of the following are true:
Middleware must run conditionally
The same route handles multiple actions
Some actions require throttling, others do not
You want explicit control over execution flow
In such cases, this pattern is valid and professional.
When You Should Remove It
If any of the following is true, the pattern is unnecessary:
The entire route can be rate-limited
OTP and login are separate routes
Simplicity is preferred over micro-optimization
Preferred Alternative (Simpler)
router.post("/admin", customLimiter, async (req, res) => {
// entire route is protected
});
This approach:
Eliminates
runMiddlewareRemoves
res.headersSentchecksIs easier to read and debug
Is sufficient for most applications
Professional Recommendation
Do not introduce
runMiddlewareunless you truly need conditional middleware execution.
In backend systems, fewer abstractions often mean:
Easier debugging
Clearer control flow
Lower cognitive load
Use runMiddleware deliberately, not defensively.
Summary
| Concern | Explanation |
|---|---|
Why runMiddleware exists | To await callback-based middleware |
Why res.headersSent is checked | Middleware may already send a response |
| Is it mandatory? | Only for conditional middleware |
| Best default choice | Route-level middleware |
Closing Thoughts
Understanding why a pattern exists is more important than memorizing it. Once you understand the execution model of Express, decisions like these become straightforward engineering trade-offs—not magic.
If your goal is clarity and maintainability, always start simple and introduce complexity only when it is justified.
Comments
Post a Comment