Errors and logging

Fail in one place, answer the client honestly, and leave a log you can actually search.

13of 18
≈ 11Min read
7Parts

Two kinds of error

Everything else in this lesson follows from telling them apart. Handling an operational error is normal; catching a programmer error and carrying on hides a bug.

OperationalProgrammer
ExamplesDatabase down, 404, invalid input, timeoutundefined is not a function, a typo, a bad assumption
Expected?Yes — they will happenNo — it is a bug
AnswerA status and a clear message500, and a log entry you go and fix
Recover?Retry, fall back, tell the userYou cannot; the state is unknown

An error class that carries a status

the small class every project ends up writing
class HttpError extends Error {
  constructor(status, message, options) {
    super(message, options);         // { cause } is preserved
    this.name = "HttpError";
    this.status = status;
    this.expose = status < 500;      // may this message be shown to a client?
  }
}

const notFound = () => new HttpError(404, "Not found");
const badRequest = (msg) => new HttpError(400, msg);

throw new HttpError(409, "That door already exists");
cause keeps the original

throw new HttpError(502, "Payment provider unreachable", { cause: err }) — the stack of the real failure is still there when you log it.

One error handler

  • It goes last, after every route and after the 404 handler.
  • A 500 message never reaches the client: stack traces and database errors tell an attacker how you are built.
  • Send a request id with the response, and log it — a user can quote it and you can find the exact request.
four arguments make it an error handler in Express
app.use((err, req, res, next) => {
  const status = err.status ?? 500;

  // the full truth goes to the log
  logger.error({ err, url: req.url, method: req.method, requestId: req.id });

  // and only what is safe goes to the client
  res.status(status).json({
    error: err.expose ? err.message : "Server error",
    requestId: req.id,
  });
});

Getting errors into it

next(err) is the only way into the error handler. Calling it from a catch is what connects your asynchronous code to the one place that formats errors.

sync throws by itself; async needs help
// Express 4: a rejected promise reaches nobody, and the request hangs
app.get("/doors", async (req, res) => { throw new Error("boom"); });

const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get("/doors", wrap(async (req, res) => { … }));

// Express 5 forwards rejections on its own

Never swallow

three bad habits
try { await save(); } catch (e) {}                     // silence
try { await save(); } catch (e) { console.log("err"); }  // useless log
catch (e) { throw new Error("Save failed"); }            // the original is gone

// handle what you can, and pass on what you cannot
catch (err) {
  if (err.code === "ENOENT") return null;
  throw new HttpError(500, "Could not save the door", { cause: err });
}

The two process-level handlers

log, then leave
process.on("uncaughtException", (err) => {
  logger.fatal({ err }, "uncaught exception");
  process.exit(1);          // the state is unknown — let the manager restart us
});

process.on("unhandledRejection", (reason) => {
  logger.fatal({ reason }, "unhandled rejection");
  process.exit(1);
});
These are not a safety net

Carrying on after an uncaught exception means running with corrupted state. Log it, exit, and let a process manager start a clean one — a restart is a second of downtime; a corrupted process is a day of confusion.

  • Use pino or winston; log JSON in production and pretty text in development.
  • Levels that mean something: error needs a human, warn is suspicious, info is a business event, debug is off in production.
  • Never log secrets, tokens, passwords or whole request bodies. Redact by key.
  • A request id on every line, passed through to downstream calls.
structured, not prose
// unsearchable
console.log("User " + id + " ordered door " + doorId);

// one JSON object per line: filter by any field, in any tool
logger.info({ userId: id, doorId, event: "order_created" }, "order created");

Practice