1. Introduction 🐞
Robust Node.js applications don't just handle the happy path — they anticipate failure. This tutorial covers everything from the basics of try/catch to advanced debugging with the Node Inspector, giving you the tools to build resilient, debuggable applications.
2. Understanding Errors ⚠️
An error represents something going wrong — invalid input, a failed network call, a bug in logic. Node distinguishes between operational errors (expected failures like a missing file) and programmer errors (bugs, like calling a method on undefined).
3. Error Types 🏷️
| Type | Description |
|---|---|
| Error | Generic base error type |
| TypeError | Value is not of the expected type |
| RangeError | Value outside the allowed range |
| ReferenceError | Reference to an undeclared variable |
| SyntaxError | Invalid code structure, usually at parse time |
4. The Error Object 📦
error-object.js
const err = new Error('Something went wrong');
console.log(err.message); // 'Something went wrong'
console.log(err.name); // 'Error'
console.log(err.stack); // full stack trace string5. try...catch 🎣
try-catch.js
try {
JSON.parse('not valid json');
} catch (err) {
console.error('Parsing failed:', err.message);
}Important
6. throw 🚀
throw-example.js
function validateAge(age) {
if (age < 0) {
throw new RangeError('Age cannot be negative');
}
return age;
}7. finally 🏁
The finally block runs regardless of whether the try block succeeded or an error was thrown and caught — ideal for cleanup like closing connections.
finally-example.js
function readConfig() {
const handle = openFile();
try {
return parseConfig(handle);
} finally {
closeFile(handle); // always runs
}
}8. Custom Errors 🎨
Subclassing Error lets you attach domain-specific data and enables checking error types with instanceof.
custom-error.js
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
try {
throw new ValidationError('Email is required', 'email');
} catch (err) {
if (err instanceof ValidationError) {
console.log(`Invalid field: ${err.field}`);
}
}9. Synchronous Error Handling 🔗
Synchronous errors propagate up the call stack until caught by a try/catch block, or, if uncaught, crash the process.
10. Asynchronous Error Handling ⏱️
Async errors don't follow the normal call stack — a throw inside a setTimeout callback or unhandled I/O callback cannot be caught by a surrounding try/catch.
async-error-pitfall.js
try {
setTimeout(() => {
throw new Error('This escapes the try/catch!');
}, 100);
} catch (err) {
console.log('This never runs'); // the throw happens in a later tick
}11. Promise Error Handling 🤝
promise-error-handling.js
async function loadData() {
try {
const data = await fetchData();
return data;
} catch (err) {
console.error('Failed to load data:', err.message);
throw err; // re-throw if the caller needs to know
}
}12. Stream Error Handling 🌊
stream-error-handling.js
const fs = require('node:fs');
const stream = fs.createReadStream('./missing.txt');
stream.on('error', (err) => {
console.error('Stream error:', err.message);
});Danger
13. EventEmitter Errors 🔔
As covered in the Events tutorial, emitting 'error' without any registered listener throws synchronously and crashes the process by default.
emitter-error.js
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('error', (err) => console.error('Handled:', err.message));
emitter.emit('error', new Error('Something broke'));14. Process Errors 🖥️
14.1 Uncaught Exceptions
uncaught-exception.js
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
process.exit(1); // recommended: exit after logging, state may be corrupt
});Danger
14.2 Unhandled Promise Rejections
unhandled-rejection.js
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection:', reason);
});Warning
15. Error Logging 📝
error-logging.js
function logError(err, context = {}) {
console.error(JSON.stringify({
message: err.message,
stack: err.stack,
timestamp: new Date().toISOString(),
...context,
}));
}16. Stack Traces 📚
A stack trace lists the chain of function calls leading up to where an error was thrown, making it invaluable for locating the root cause.
stack-trace.js
function a() { b(); }
function b() { c(); }
function c() { throw new Error('Deep error'); }
try {
a();
} catch (err) {
console.log(err.stack);
// Error: Deep error
// at c (...)
// at b (...)
// at a (...)
}17. Debugging Tools 🔍
17.1 Debugging with Node Inspector
node-inspector.sh
node --inspect index.js
# or break on the first line:
node --inspect-brk index.jsTip
17.2 Chrome DevTools
Navigate to chrome://inspect in Chrome, click "inspect" next to your running Node process, and use the familiar Sources panel to set breakpoints, step through code, and inspect variables.
17.3 VS Code Debugger
.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug App",
"program": "${workspaceFolder}/index.js",
"skipFiles": ["<node_internals>/**"]
}
]
}Tip
18. Logging Strategies 🗒️
Beyond console.log, production systems typically use a structured logging library (like pino or winston) that supports log levels, JSON output, and log shipping.
- Use log levels (debug, info, warn, error) to control verbosity.
- Log structured data (JSON) rather than free-form strings for easier querying.
- Include context — request ID, user ID, timestamp — with every log entry.
19. Source Maps 🗺️
When running transpiled or minified code (TypeScript, bundlers), source maps let stack traces and debuggers point back to your original source lines instead of generated output.
source-map-support.sh
node --enable-source-maps index.js20. Production Debugging 🏭
- Use structured logs and centralized log aggregation (e.g. ELK, Datadog) rather than relying on local console output.
- Capture heap snapshots (--heapsnapshot-signal) to diagnose memory leaks without restarting the process.
- Use process.on('warning') to catch Node's own runtime warnings.
- Consider APM tools for tracing errors across distributed services.
21. Error Recovery 🩹
Not every error should crash the process. Graceful degradation — falling back to cached data, retrying with backoff, or returning a partial response — often keeps a service usable even when a dependency fails.
error-recovery.js
async function getUserData(id) {
try {
return await fetchFromPrimaryDB(id);
} catch (err) {
console.warn('Primary DB failed, falling back to cache:', err.message);
return await fetchFromCache(id);
}
}22. Best Practices ✅
- Always distinguish operational errors (expected) from programmer errors (bugs) in how you respond.
- Use custom Error subclasses to carry structured context about what failed.
- Log errors with full stack traces and relevant context, not just message.
- Treat uncaughtException and unhandledRejection as safety nets — fix the root cause instead of relying on them.
- Exit and restart (via a process manager) after an uncaughtException, rather than trying to continue in a potentially corrupted state.
23. Common Mistakes ⚠️
- Wrapping asynchronous callback-based code in try/catch and expecting it to catch async errors.
- Swallowing errors silently (empty catch blocks) instead of logging or handling them.
- Not attaching an 'error' listener on streams or emitters.
- Relying on uncaughtException to keep the process alive indefinitely instead of exiting and restarting.
- Losing the original stack trace by re-throwing a new error without setting its cause or preserving context.