Handbooks / Node.js / Chapter 5
Error Handling and Async Patterns
7 pages · ~5 min✓ Reviewed
Builds on Express Routing and Middleware.
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) erris null or undefined on success
2. Callbacks vs promises
| Callback | Promise | |
|---|---|---|
| Return or await | No | Yes |
| Nesting | callback hell | flat chain |
| Errors | cb(err) | .catch |
3. Example: readFile
- Check
errfirst and return util.promisifywraps 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
returncallscbtwice.
Remember
if (err) return cb(err) - the return stops the second call.
Promises and async/await
1. Promise basics
- Pending, then fulfilled or rejected, exactly once
.thenhandles success,.catchrejection,.finallyalways runsasyncfunctions always return a promiseawaitpauses only that function; a rejection becomes a throw for try/catch
2. Combinators
| Method | Settles |
|---|---|
Promise.all | Rejects fast on first failure |
allSettled | Waits for all, reports each outcome |
race | First to settle, either way |
any | First success |
- Awaits in a row run sequentially;
Promise.allruns 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 awaitinsideforEachdoes not wait; usefor...oforPromise.allwithmapreturn pinsidetrywithoutawait: 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
Express Error Middleware
1. Definition and Mechanism
- Error handler has 4 params:
(err, req, res, next) - Found by arity: keep
nexteven if unused next(err)or a syncthrowskips to it- Register it last, after routes and
app.use
2. Express 4 vs 5
| Express 4 | Express 5 | |
|---|---|---|
| Async rejection | Never reaches next: request hangs, Node 15+ may crash | Forwarded automatically |
| Fix | Wrap, or catch-to-next helper | None needed |
3. Example
AppErrorcarries 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, thennext(err) - Stack traces leaked in production
- Missing 404 handler before the error handler: unmatched routes skip your JSON format
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
| Event | Callback arg | Do |
|---|---|---|
unhandledRejection | reason | log, exit |
uncaughtException | err | log, 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); });
- Error
- Log
- exit(1)
- 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)
Environment Config and Graceful Shutdown
1. Env Config
process.envholds strings only- Local:
.envvia 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
- SIGTERM / SIGINT
- server.close()no new connections
- Drainin-flight requests
- Close DB, queues
- 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.closeopen - 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
Cheat Sheet: Async, Errors and Shutdown
1. Callbacks and Promises
- Shape is
cb(err, result): check err first andreturn util.promisifyconverts it into a promise
| Method | Settles when |
|---|---|
Promise.all | first rejection (fails fast) |
Promise.allSettled | all done, every outcome reported |
Promise.race | first to settle |
Promise.any | first success |
2. async/await and Express
- Always await or
returnthe promise - Use
for...oforPromise.allwithmap - 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
unhandledRejectionanduncaughtException: log and exit- Validate env at boot
Graceful shutdown
- 1SIGTERM
- 2server.closestop new requests
- 3CleanupDB, queues
- 4Exittimeout forces it
Forced exit
Always set a timeout that calls process.exit(1), or open connections can hang the shutdown.
Check yourself
What logs first, and why? items.forEach(async id => await save(id)); console.log('done')
donelogs first: forEach ignores the promises its callbacks return- Fix:
for...ofwith await, orawait 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