NodeJSError Handling in Node.js

Error Handling in Node.js

Every non-trivial program encounters errors: a network call times out, a file is missing, a database constraint fires, user input is invalid. How you handle those errors determines whether your app crashes and burns, silently corrupts data, or degrades gracefully while staying up. Node's async nature adds extra complexity — a thrown error inside a callback or an unhandled rejected promise can crash the process if you aren't careful. This section covers the full picture: the error types Node recognises, try/catch with async/await, the operational-vs-programmer distinction, custom error classes, centralised handling, and the two process-level traps — uncaught exceptions and unhandled rejections.

The three channels errors travel through

Text
1. Synchronous throw
      throw new Error('bad input')    → caught by surrounding try/catch

2. Rejected promise / async throw
      async function f() { throw new Error() }
      await f()                       → caught by try/catch around the await
      f()                             → UNHANDLED rejection if no .catch()

3. EventEmitter 'error' event
      stream.on('error', handler)     → unhandled 'error' throws synchronously
An unhandled rejected promise will crash the process in Node 15+
Before Node 15, an unhandled promise rejection printed a warning. From Node 15 onwards it **crashes the process** by default — the same behaviour as an uncaught synchronous throw. Every `Promise` chain needs a `.catch()` or an `await` inside a `try/catch`. In practice: always `await` async work in Express handlers (or use a wrapper that catches and forwards to `next`), and listen for `process.on('unhandledRejection')` as a last-resort safety net.
What good error handling looks like end-to-end

Text
Request comes in
  ↓ route handler — try/catch around all async work
  ↓ on error: call next(err) with a meaningful Error object
  ↓ Express error middleware (4-arg) catches it
  ↓ classifies: operational (send HTTP error) vs programmer (log + 500)
  ↓ sends a safe, structured JSON error response
  ↓ process stays up
The goal: every error ends up in one place, classified, logged, and responded to
Scattered `try/catch` blocks that each format their own error response create inconsistency and duplicate logic. The pattern throughout this section is: catch errors as close to the source as needed, wrap them in a meaningful Error object, call `next(err)`, and let **one central error handler** decide what to log and what to send to the client. This keeps error-response formatting consistent and your handlers clean.
What this section covers

Page

Covers

The Error object, system errors, HTTP errors

async/await error propagation patterns

Which errors to handle vs which to crash on

Subclassing Error for domain-specific types

Express 4-arg error middleware

process.on('uncaughtException')

process.on('unhandledRejection')

Draining connections before exit

The cardinal rules
  • Never swallow an error silently — at minimum log it; ideally propagate it.

  • Never expose internal details to the client — stack traces, SQL queries, file paths.

  • Use next(err) in Express — don't throw inside async handlers without catching first.

  • Classify before responding — operational errors get HTTP status codes; programmer errors get logged and fixed.

  • Keep the process up for operational errors; consider controlled restart for programmer errors.

Never send a stack trace or internal error detail to the client
A stack trace reveals file paths, library versions, and code structure — a gift to an attacker probing your app. When an error reaches your centralised handler, log the full detail **server-side** and send the client only a safe, generic message (and an error code/id they can quote to support). The client needs to know *what went wrong* at the user level, not *how your server is built* at the implementation level.
Next
Start with the building block: [Error Types & the Error Object](/nodejs/error-types).