This lesson goes one level deeper than a simple checklist, explaining why each of the most common Express mistakes actually causes problems, so you recognize the underlying pattern, not just the specific example.
Why the Same Mistakes Keep Happening
Most Express mistakes aren't caused by not knowing the framework, they're caused by assumptions that feel reasonable in the moment: "this route will only ever get valid input", "I'll add error handling later", "this will only ever run on one server". This lesson walks through the most common assumptions that eventually break.
// looks fine in isolation...
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id); // what if this rejects?
res.json(user); // what if user is null?
});
Neither an unhandled rejection nor a null user is handled here, both are extremely common real-world outcomes, not edge cases.
Most mistakes fall into a small number of recurring categories, covered below.
Recognizing the category helps you spot similar mistakes elsewhere in a codebase.
Code review checklists are far more effective when built around these categories.
Common Mistakes Quick Reference
A condensed list mapping each mistake to its fix.
Mistake
Why It Breaks
Fix
Missing next() call
Request hangs forever with no response
Ensure every code path responds or calls next()
Unhandled async rejection
Crashes process or silently fails
try/catch or an asyncHandler wrapper
No input validation
Malformed data crashes routes or corrupts data
Validate every client input
Route order bug
Specific route shadowed by an earlier generic one
Register literal paths before parameterized ones
New DB connection per request
Exhausts connection limits under load
Use a shared connection pool
Async Error Handling Mistakes
The single most common category of Express bugs in real codebases involves async code: a rejected promise inside an async route handler that nothing ever catches. In Express 4, this doesn't crash loudly, it either hangs the request or silently produces an unhandled rejection warning in your logs, with the client left waiting.
// BAD: rejected promise never reaches the error handler
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
res.json(user);
});
// GOOD
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
}));
"This Will Only Ever Run on One Server" Assumptions
In-memory sessions, in-memory rate limiting, and in-memory caches all work perfectly, until the app is deployed with more than one instance behind a load balancer, at which point behavior becomes inconsistent depending on which instance handles a given request. Assume from the start that your app might run as multiple instances, even if it doesn't yet.
Common Mistakes
Assuming a route will only ever receive well-formed input.
Assuming an async operation will always resolve successfully.
Assuming the app will only ever run as a single instance.
Assuming "I'll add proper error handling/tests/security later" without a concrete plan to do so.
Key Takeaways
Most Express mistakes come from reasonable-sounding assumptions that eventually break under real conditions.
Async error handling is the single most common source of subtle production bugs.
Design for multiple server instances from the start, even if you only run one today.
Recognizing the underlying category of a mistake helps you catch similar ones elsewhere.
Pro Tip
When reviewing a pull request, explicitly ask "what happens if this async call rejects?" and "what happens if this input is missing or malformed?", these two questions catch the majority of real-world Express bugs before they ship.
You now understand common Express mistakes in depth. Next, use the full Express.js Cheat Sheet as a quick reference.