Handbooks / Node.js / Chapter 5

Error Handling and Async Patterns

7 pages · ~5 min✓ Reviewed

Builds on Express Routing and Middleware.

Node.jsPage 1

Callbacks and the Error-First Convention

1. Definition and signature

  • Callback = function passed to an async API
  • The event loop calls it later, never inline in the caller's stack
  • Error-first: cb(err, result)
  • err is null or undefined on success

2. Callbacks vs promises

CallbackPromise
Return or awaitNoYes
Nestingcallback hellflat chain
Errorscb(err).catch

3. Example: readFile

  • Check err first and return
  • util.promisify wraps this style into a promise
javascript
function read(path, cb) {
  fs.readFile(path, 'utf8', (err, data) => {
    if (err) return cb(err);
    cb(null, data);
  });
}
const readP = util.promisify(read);

4. Interview Point

  • Why never throw in an async callback? The try/catch has already exited; pass cb(err).
  • What does promisify require? An error-first (err, value) callback as the last argument.
  • Double-callback bug? A missing return calls cb twice.
Remember

if (err) return cb(err) - the return stops the second call.

Node.jsPage 2

Promises and async/await

1. Promise basics

  • Pending, then fulfilled or rejected, exactly once
  • .then handles success, .catch rejection, .finally always runs
  • async functions always return a promise
  • await pauses only that function; a rejection becomes a throw for try/catch

2. Combinators

MethodSettles
Promise.allRejects fast on first failure
allSettledWaits for all, reports each outcome
raceFirst to settle, either way
anyFirst success
  • Awaits in a row run sequentially; Promise.all runs them in parallel
javascript
const [a, b] = await Promise.all([getA(), getB()]) // parallel
await getA(); await getB() // sequential

3. Mistakes

  • Forgetting await: the promise is silently dropped
  • await inside forEach does not wait; use for...of or Promise.all with map
  • return p inside try without await: the catch is skipped

4. Interview Point

  • Does await block the event loop? No, only its own function pauses
  • Run tasks in parallel? Start them all, then Promise.all
  • all vs allSettled? Fail fast vs wait and report every outcome
Node.jsPage 3

Express Error Middleware

1. Definition and Mechanism

  • Error handler has 4 params: (err, req, res, next)
  • Found by arity: keep next even if unused
  • next(err) or a sync throw skips to it
  • Register it last, after routes and app.use

2. Express 4 vs 5

Express 4Express 5
Async rejectionNever reaches next: request hangs, Node 15+ may crashForwarded automatically
FixWrap, or catch-to-next helperNone needed

3. Example

  • AppError carries an HTTP status; the handler maps it to the response
javascript
class AppError extends Error {
  constructor(status, msg) { super(msg); this.status = status }
}
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({ error: err.message })
})

4. Mistakes

  • Double response: check res.headersSent, then next(err)
  • Stack traces leaked in production
  • Missing 404 handler before the error handler: unmatched routes skip your JSON format
Node.jsPage 4

Unhandled Rejections and Uncaught Exceptions

1. What Fires

  • unhandledRejection fires when a promise rejects with no handler attached
  • Node 15+: default is crash with a non-zero exit

2. Last-Resort Hooks

EventCallback argDo
unhandledRejectionreasonlog, exit
uncaughtExceptionerrlog, exit
  • Hooks are for logging, not recovery
  • After uncaughtException the state is unknown
  • Never keep serving as if nothing happened

3. Log, Exit, Restart

javascript
process.on('unhandledRejection', (r) => {
  logger.error(r);
  process.exit(1);
});
  1. Error
  2. Log
  3. exit(1)
  4. SupervisorPM2, Docker, Kubernetes
Handle it earlier

Catch errors with try/catch, .catch and next(err). The hooks are the net.

  • Node 15 change? Unhandled rejections now crash the process
  • Unsafe after uncaughtException? State is unknown
  • Where to handle errors? At the source: try/catch, .catch, next(err)
Node.jsPage 5

Environment Config and Graceful Shutdown

1. Env Config

  • process.env holds strings only
  • Local: .env via dotenv or --env-file; never commit it
  • Real values are set in the platform
  • Fail fast at boot on missing vars
  • Convert types: Number(process.env.PORT)
  • Defaults only for non-secrets

2. Shutdown on SIGTERM

  1. SIGTERM / SIGINT
  2. server.close()no new connections
  3. Drainin-flight requests
  4. Close DB, queues
  5. exit(0)
javascript
process.on('SIGTERM', () => {
  server.close(async () => {
    await db.end()
    process.exit(0)
  })
  setTimeout(() => process.exit(1), 10000).unref()
})

forced exit after 10 s

3. Mistakes

  • Ignoring SIGTERM in containers: killed after the grace period
  • Keep-alive connections hold server.close open
  • Exiting before cleanup finishes

4. Interview Point

  • Why SIGTERM, not SIGKILL? SIGTERM can be caught to clean up; SIGKILL cannot
  • Why a forced-exit timeout? Hung connections would block exit forever
  • Why validate env at boot? Fail at deploy, not on the first request
Node.jsPage 6

Cheat Sheet: Async, Errors and Shutdown

1. Callbacks and Promises

  • Shape is cb(err, result): check err first and return
  • util.promisify converts it into a promise
MethodSettles when
Promise.allfirst rejection (fails fast)
Promise.allSettledall done, every outcome reported
Promise.racefirst to settle
Promise.anyfirst success

2. async/await and Express

  • Always await or return the promise
  • Use for...of or Promise.all with map
  • Never forEach: it does not wait
  • Four-argument handler (err, req, res, next) goes last
  • next(err) forwards errors
  • Express 4: wrap async handlers

3. Process and Shutdown

  • unhandledRejection and uncaughtException: log and exit
  • Validate env at boot
Graceful shutdown
  1. 1SIGTERM
  2. 2server.closestop new requests
  3. 3CleanupDB, queues
  4. 4Exittimeout forces it
Forced exit

Always set a timeout that calls process.exit(1), or open connections can hang the shutdown.

Node.jsPage 7

Check yourself

What logs first, and why? items.forEach(async id => await save(id)); console.log('done')
  • done logs first: forEach ignores the promises its callbacks return
  • Fix: for...of with await, or await Promise.all(items.map(save))
Promise.all([a(), bFails(), c()]): what happens, and what would allSettled give instead?
  • all rejects at once with b's error, and the results of a and c are lost
  • allSettled never rejects: it reports all three outcomes
In Express 4, an async handler throws. Why doesn't the error middleware run?
  • Express 4 ignores the rejected promise the handler returns
  • The rejection goes unhandled, which crashes Node 15+ by default
  • Fix: catch and call next(err), or wrap the handler; Express 5 forwards it