04 — Error Handling in Node.js — Knowing Which Error You Have
"Wrap it in try/catch and move on" was my error strategy, and it treated every error like the same problem. The model that finally stuck: errors come in distinct kinds, and the right response depends entirely on which kind you're holding. A network glitch and a TypeError from my own code look similar in a log, but one is recoverable and the other is a bug [1].
The framing that landed for me is the taxonomy. There are four kinds worth separating, and each has a different remedy.
The operating-error versus programmer-error split
Before the taxonomy, the single distinction that matters most is operational error versus programmer error [1]:
- Operational error — something went wrong in a correct program. The file was missing, the database was down, the user sent bad input. The code is fine; the world was uncooperative. The right response is to handle it — retry, fall back, return a 4xx to the client.
- Programmer error — a bug. I called a function with the wrong type, referenced an undefined variable, mutated a shared structure. The code is wrong; no amount of retrying will fix it because the same input will fail the same way. The right response is to log it, crash, and let the process restart in a clean state.
This split is the reason "catch everything and keep going" is a trap. Catching a programmer error and continuing means running a program in a corrupted state, which produces worse failures downstream. Catching an operational error and crashing means a transient blip takes down a server. Knowing which is which is most of the job.
System errors
System errors happen when Node.js asks the operating system to do something and the OS refuses. Reading a missing file, connecting to a port nothing is listening on, hitting a broken pipe. These are instances of Node's SystemError class and carry useful properties — code (like ENOENT or ECONNREFUSED), syscall (like open or connect), and errno [2].
import { readFileSync } from 'fs';
try {
readFileSync('/does/not/exist');
} catch (err) {
console.log(err.code); // 'ENOENT'
console.log(err.syscall); // 'open'
}System errors are operational errors almost by definition — the OS state caused them, and retrying or degrading gracefully is the right move. The code property is the part I branch on, because the error message text can vary by locale but ENOENT is stable.
User-specified errors
When I want to signal a domain problem to the caller — "you tried to withdraw more than your balance," "that email is already registered" — I throw my own error, and the idiomatic way is to extend the base Error class [3].
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
throw new ValidationError('Email already registered', 'email');Subclassing gives me a name to switch on at the call site, plus whatever domain-specific properties (field, code, statusCode) the handler needs. The stack property is inherited and points at where the error was thrown — useful in logs, since the stack trace is how I trace back from a symptom to the line that caused it [4].
Assertion errors
Assertion errors are a special case used mostly in tests and runtime invariants. The built-in assert module throws an AssertionError when an expression is not truthy [5].
import assert from 'assert/strict';
assert.strictEqual(response.status, 200);
// throws AssertionError if status !== 200In test code, assertions are the whole mechanism — the test runner catches them and reports pass/fail. In application code I use them sparingly, for invariants that should be impossible to violate: "if we got here, the user object must be loaded." An assertion firing in production is a programmer error by definition, and crashing is the correct response.
JavaScript errors
The last kind is the language itself complaining. TypeError (calling a method on undefined), RangeError (a number out of bounds), ReferenceError (an undefined variable), SyntaxError (the parser gave up). These are almost always programmer errors — they mean my code is wrong [6]. The MDN error reference catalogs the full set, and reading the specific type usually points at the bug directly: a TypeError: Cannot read properties of undefined (reading 'map') tells me a variable I expected to be an array is actually undefined, and the fix is upstream of the line that threw.
Handling them in async code
The one wrinkle worth flagging is that try/catch only catches synchronous throws and await-ed promise rejections [1]. A rejection on a promise nobody awaited becomes an unhandledRejection, and historically that crashed the process in newer Node versions. The discipline is: every promise gets either an await inside a try/catch, a .catch(), or a deliberate "fire and forget" with a logged rejection. Errors that escape all handlers become the fatal kind, and pretending otherwise is how servers mysteriously restart.
How I use this
The habit I keep is a single question when an error fires: is this operational or a bug? Operational → handle it, maybe retry, return a clean response to the client. Bug → log the stack and let the process die (the process manager restarts it; the crash surfaces the bug instead of hiding it). I subclass Error for domain errors so handlers can switch on name or code. And I branch on err.code for system errors rather than the message text. The taxonomy — system, user, assertion, JavaScript — is what turns "something threw" into a decision I can make.
References
[1] Sematext, "Node.js Error Handling Best Practices," Sematext Blog. [Online]. Available: https://sematext.com/blog/node-js-error-handling
[2] OpenJS Foundation, "Node.js Errors — Class: SystemError," Node.js API Docs. [Online]. Available: https://nodejs.org/api/errors.html#errors_class_systemerror
[3] Honeybadger, "A Comprehensive Guide To Error Handling In Node.js," Honeybadger Blog. [Online]. Available: https://www.honeybadger.io/blog/errors-nodejs/
[4] cloudhadoop, "Multiple Ways to Log The Stack Trace in Node.js." [Online]. Available: https://www.cloudhadoop.com/nodejs/print-stack-trace-error/
[5] OpenJS Foundation, "Node.js Assert," Node.js API Docs. [Online]. Available: https://nodejs.org/api/assert.html#new-assertassertionerroroptions
[6] Mozilla, "JavaScript error reference," MDN Web Docs. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors
Knowledge check · Question 1 of 4
What is the key distinction that drives error-handling strategy in Node.js?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!