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
throwanError. A thrown string has no stack and noinstanceoftest. super(message, { cause })preserves the original failure, so a log can print the whole chain witherr.cause.- System errors carry a
codesuch asENOENTorECONNREFUSED; branch on that rather than matching message text. - Set
this.nameso stack traces and logs identify your error type instead of the genericError. - Use
AggregateErrorwhen 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 code | Meaning |
|---|---|
| 0 | Completed successfully |
| 1 | Runtime failure the program could not handle |
| 2 | Usage error — bad flags or missing arguments |
| 128 + N | Killed by signal N, so 137 means SIGKILL, often an out-of-memory kill |
- Prefer
process.exitCode = 1overprocess.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
debuggerstatement pauses execution only when a debugger is attached, so it is safe to leave in code. --inspectis a remote code execution interface. Bind it to localhost and never publish the port.diagnostics_channellets 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.
Related
Configuration, environment and CLI arguments Testing with the built-in test runner
Last refreshed 2026-09-18.