Tutorials Logic, IN info@tutorialslogic.com

Node.js Error Handling and Debugging

Failure Boundaries

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 and Operational Failures

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.

  • Classify by recovery action.
  • Never retry unknown failures blindly.
  • Keep public messages separate from internal evidence.

Four Error Delivery Styles

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.

Custom Errors and Causes

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.

Translate a File Error with Cause

Translate a File Error with Cause
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.

HTTP Error Boundary

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.

Map Only Known Errors to Public Responses

Map Only Known Errors to Public Responses
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);
Output
404

Only stable known classifications become client outcomes; unexpected details remain in protected logs.

Unhandled Rejection and Uncaught Exception

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.

Inspector, Breakpoints, and Source Maps

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.

Diagnostic Workflow

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.

Failure-Path Tests

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.

Before you move on

Error Boundary Check

4 checks
  • I recognize the delivery style of each error.
  • I preserve causes and use stable classifications.
  • I keep raw internals out of client responses.
  • I debug from evidence and test failure paths.

Node JS Questions Learners Ask

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.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.