BugForge
All lessons
Node.js · 06/19 7 min

Errors in Node and Express

throw vs reject, async handlers, error middleware, custom error classes and status codes.

Error bugs are sneaky because the happy path works. The tests that fail are the ones that send bad input and expect a specific status and message.

Throw real errors

throw "not found";                 // ❌ a string: no stack, no .message, instanceof fails
throw new Error("not found");      // ✅

A small error hierarchy

class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.status = status;
    this.name = this.constructor.name;
  }
}
class NotFound extends HttpError { constructor(what) { super(404, `${what} not found`); } }
class BadRequest extends HttpError { constructor(msg) { super(400, msg); } }

One error handler then maps every error to a response:

app.use((err, req, res, next) => {               // four args, registered LAST
  const status = err.status ?? 500;
  res.status(status).json({ error: status === 500 ? "Internal error" : err.message });
});

Async handlers in Express 4

Express 4 only catches errors thrown synchronously. A rejected promise in an async handler never reaches the error middleware: the request hangs or the process logs an unhandled rejection.

// ❌ rejection escapes Express 4
app.get("/items/:id", async (req, res) => {
  const item = await Item.findById(req.params.id);
  res.json(item);
});

// ✅ forward it
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get("/items/:id", wrap(async (req, res) => { … }));

Express 5 forwards rejected promises for you, but labs and many real codebases still run Express 4.

Other classics

  • Respond once. if (!user) res.status(404).json(…) without return keeps going and sends a second response: "Cannot set headers after they are sent".
  • Don't swallow. catch (e) {} turns a 500 into a confusing 200 with an empty body.
  • Don't leak internals. Return a clean message for 500s; log the stack server-side.
  • Wrong class of status. Validation errors are 400, missing records 404, duplicates 409. A 500 in a test usually means an error wasn't mapped.
  • Unhandled rejections crash Node 15+ by default. That's a feature: it surfaces the bug.
Practise it now
Next in Node.js
Dates, timestamps and money in JS