Handle an error where code can retry, translate, compensate, or terminate safely. Preserve the cause, attach operation context, and keep internal details out of public responses.
Programmer errors violate code assumptions: invalid internal state, impossible branches, or calling an API incorrectly. Operational errors arise from expected runtime conditions such as missing files, refused connections, invalid client input, timeouts, or resource limits.
Classification guides action, but it is not an excuse to ignore either class. Reject invalid input safely, retry only classified transient operations, and terminate a process whose trusted internal invariants may be broken.
Synchronous APIs throw. Promise APIs reject. Error-first callbacks pass an error argument. Event emitters and streams may emit error. A try/catch surrounding the call site cannot catch an error delivered later by callback or event.
Know the contract of every API at the boundary. Await promises, inspect callback errors first, and attach an error listener or use pipeline for owned streams. Mixing styles without an adapter creates gaps where failures escape.
Use a small custom error type when callers need stable classification, such as validation, conflict, not found, or unavailable. Include safe structured fields such as code and status; do not require callers to parse message text.
When translating a lower-level error, pass it as cause. The outer message explains the failed operation while the cause preserves the original code and stack for logs. Avoid wrapping at every layer because noisy stacks hide the useful boundary.
import { readFile } from 'node:fs/promises';
async function loadPolicy(path) {
try {
return JSON.parse(await readFile(path, 'utf8'));
} catch (error) {
throw new Error(`Policy could not be loaded from ${path}`, { cause: error });
}
}
The operation context and original failure remain available without exposing either to an HTTP client automatically.
Validate request shape before domain work, map known outcomes to deliberate status codes, and return one safe error envelope. Record the detailed stack, cause, request ID, route template, and operation name internally. Never return raw database, file-system, or dependency errors.
A response may already have started when streaming fails. In that case status and headers cannot be replaced; terminate the response or connection as appropriate and record a partial-response failure. Design streaming formats so consumers can detect truncation.
function toPublicError(error, requestId) {
const known = {
TASK_NOT_FOUND: [404, 'Task was not found'],
INVALID_TASK: [422, 'Task data is invalid']
};
const [status, message] = known[error.code] ?? [500, 'Internal server error'];
return { status, body: { error: { code: error.code ?? 'INTERNAL', message, requestId } } };
}
console.log(toPublicError({ code: 'TASK_NOT_FOUND' }, 'req-7').status);
404
Only stable known classifications become client outcomes; unexpected details remain in protected logs.
Under current default behavior, an unhandled rejection becomes an uncaught exception. Process handlers are last-resort telemetry and shutdown boundaries, not substitutes for awaiting work. Once an exception escaped normal control flow, application state may be uncertain.
Use uncaughtExceptionMonitor when diagnostics are needed without changing default handling, or a carefully tested fatal handler that performs bounded shutdown. Do not resume ordinary request processing after a fatal exception.
Run node --inspect for a debugger connection or --inspect-brk to pause before user code. Bind the inspector only to a trusted interface; exposing it grants powerful process access. Use conditional breakpoints, watch expressions, async stacks, CPU profiles, and heap snapshots with production-data care.
Enable source maps when compiled code is deployed and verify stack locations in the released artifact. A breakpoint changes timing, so reproduce races with structured logs, traces, or deterministic tests as well as interactive debugging.
Start with the exact failure, input class, runtime version, deployment revision, and correlation ID. Reproduce minimally, inspect the first application frame and cause chain, form one hypothesis, then add the smallest observation that can disprove it.
Use --trace-warnings for warning origins, diagnostic reports for process state, heap snapshots for retained memory, and CPU profiles for hot code. These artifacts can contain secrets and user data; control access and retention.
Test malformed input, timeouts, aborts, dependency refusal, partial writes, duplicate completion, cleanup failure, and fatal startup errors. Assert status, safe response, internal classification, and final resource state.
Fault injection should be deterministic. Replace a boundary with a controlled failing implementation or use an isolated dependency; do not make the suite depend on random timing. A passing happy path says little about reliability.
No. It catches synchronous throws and awaited rejections inside its scope, but later callback or event errors follow their own delivery contracts.
It should normally record the fatal failure, stop accepting work, perform bounded cleanup, and let a supervisor restart it because trusted state may be compromised.
Explore 500+ free tutorials across 20+ languages and frameworks.