Skip to content

Common Express.js Mistakes

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.

Categories of Common Mistakes

Routing & middleware mistakes
Async & error handling mistakes
Security mistakes
Database & performance mistakes
  • 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.