Errors, logging and debugging

Custom error classes with cause chaining, exit codes that mean something, structured logs, and the built-in inspector.

Error classes and cause chaining

class AppError extends Error {
  constructor(message, options) {
    super(message, options);        // options.cause keeps the original error
    this.name = "AppError";
    this.status = 500;
  }
}

class NotFoundError extends AppError {
  constructor(id) {
    super("no order " + id, { cause: new Error("query returned no rows") });
    this.name = "NotFoundError";
    this.status = 404;
  }
}

try {
  await loadOrder(9);
} catch (err) {
  throw new AppError("checkout failed", { cause: err });
}
  • Always throw an Error. A thrown string has no stack and no instanceof test.
  • super(message, { cause }) preserves the original failure, so a log can print the whole chain with err.cause.
  • System errors carry a code such as ENOENT or ECONNREFUSED; branch on that rather than matching message text.
  • Set this.name so stack traces and logs identify your error type instead of the generic Error.
  • Use AggregateError when several failures must be reported together.

Exit codes and structured logging

const log = (level, message, fields = {}) =>
  process.stdout.write(
    JSON.stringify({ time: new Date().toISOString(), level, message, ...fields }) + "\n"
  );

log("info", "order created", { orderId: 1042, userId: 7 });

process.on("uncaughtException", (err) => {
  log("fatal", "uncaught exception", { message: err.message, stack: err.stack });
  server.close(() => process.exit(1));
});
Exit codeMeaning
0Completed successfully
1Runtime failure the program could not handle
2Usage error — bad flags or missing arguments
128 + NKilled by signal N, so 137 means SIGKILL, often an out-of-memory kill
  • Prefer process.exitCode = 1 over process.exit() while cleanup is pending: exit() truncates buffered output and in-flight work.
  • Write logs to stdout and let the platform collect them; a log file inside a container disappears with the container.
  • Include a request or correlation id in every line so one user action can be reassembled across services.
  • Never log passwords, tokens, card numbers or whole request bodies.
  • Reach for pino or winston when you need levels, transports or redaction; a hand-rolled JSON line is fine for small services.

Debugging a running process

node --inspect-brk src/server.js    # pause on the first line, open chrome://inspect
node --inspect=0.0.0.0:9229 app.js  # only inside a trusted container
node --watch src/server.js          # restart on file change, development only
node --trace-warnings app.js
node --stack-trace-limit=50 app.js
node --cpu-prof src/worker.js       # writes a .cpuprofile you can open in DevTools

kill -USR1 <pid>                    # start the inspector on an already-running process
  • A debugger statement pauses execution only when a debugger is attached, so it is safe to leave in code.
  • --inspect is a remote code execution interface. Bind it to localhost and never publish the port.
  • diagnostics_channel lets libraries publish instrumentation that your own tracing can subscribe to without a hard dependency.
  • Reproduce first: a minimal script that fails deterministically beats stepping through a large application.
  • When the loop is blocked, a CPU profile tells you which frame is responsible far faster than reading code.
⚠️
An open inspector port is a shell on your server. If you forward it into a container, treat that tunnel as production access and close it when you are done.

FAQ

Should the process restart after an uncaught exception?
Yes, but only through the supervisor — log the failure, let in-flight work finish within a deadline, then exit non-zero and let the platform start a clean process. Continuing after an unknown failure risks corrupted state.
console.log or a logging library?
For a small service, structured JSON on stdout is enough. Move to a library when you need levels, sampling, redaction or multiple transports — those are the things you will otherwise reimplement badly.

Configuration, environment and CLI arguments Testing with the built-in test runner

Last refreshed 2026-09-18.