Suggest an editImprove this articleRefine the answer for “Error handling patterns in Node.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Node.js error handling patterns** - the conventions for catching and propagating errors in callback, Promise, and async/await code. ```js async function getUser(id) { try { return await db.users.findById(id); } catch (err) { console.error(err.message); throw err; // re-throw so Express error middleware handles the response } } ``` **Key point:** always re-throw errors from async functions unless you return a specific fallback, or the caller receives `undefined` instead of the error.Shown above the full answer for quick recall.Answer (EN)Image**Node.js error handling patterns** - the conventions for catching and propagating errors across callback, Promise, and async/await code so a single unhandled failure does not crash the whole server. ## Theory ### TL;DR - Two categories: **operational errors** (DB timeout, file not found) you handle and recover from; **programmer errors** (TypeError, bad argument) you fix and restart - Callbacks use error-first convention: first argument is always `err` or `null` - Promises use `.catch()`, async functions use `try/catch` - Express needs a 4-argument middleware `(err, req, res, next)` placed after all routes - `uncaughtException` must call `process.exit(1)` - the process state is corrupted after one fires ### Quick example The most common modern pattern: ```js async function loadUserProfile(userId) { try { const user = await db.users.findById(userId); if (!user) { throw new NotFoundError('User'); // custom error carries status code } return user; } catch (err) { logger.error({ userId, err }, 'Failed to load user profile'); throw err; // re-throw so Express error middleware handles the response } } ``` One `try/catch` wraps every `await` in the function. Any rejection from any of them lands in `catch`. No per-call `.catch()` chaining needed. ### Operational vs programmer errors This distinction comes up in every senior-level interview on this topic. Operational errors are expected: a database connection drops, a user sends invalid input, an S3 file does not exist. You anticipate these and write code that recovers - return a 404, retry once, log a warning. Programmer errors are bugs. A `TypeError: Cannot read properties of undefined` means the code is wrong. You do not recover from these gracefully. You log them, exit, and let PM2 or another process manager restart the server. Mixing the two is where most production incidents start. ### Error-first callbacks Before Promises, every async Node.js API used this convention: ```js const fs = require('fs'); fs.readFile('./config.json', 'utf8', (err, data) => { if (err) { // err.code is 'ENOENT' for missing file, 'EACCES' for permission denied console.error('Could not read config:', err.code); return; // stop - data is undefined here } const config = JSON.parse(data); startServer(config); }); ``` Always check `err` first, always `return` after handling it. Skip the `return` and the code below runs with `data` being `undefined`. This pattern still appears in Node.js core APIs (`fs`, `dns`, `crypto`) and in older codebases. ### Promises and .catch() Errors propagate down the Promise chain until a `.catch()` picks them up: ```js fetchUser(id) .then(user => enrichWithPosts(user)) // rejection here skips to .catch .then(enriched => formatResponse(enriched)) .catch(err => { console.error('Pipeline failed:', err.message); return { error: err.message }; // return a fallback to recover }) .finally(() => metrics.record('fetchUser')); // runs on success and failure ``` `.finally()` is good for cleanup that should always happen: closing connections, recording timing metrics, releasing locks. ### async/await with try/catch `async/await` is syntactic sugar over Promises. The error model is identical, but the code reads like synchronous logic: ```js async function processOrder(orderId) { let order; try { order = await db.orders.findById(orderId); } catch (err) { throw new DatabaseError('Could not fetch order', err); } try { await paymentService.charge(order.totalAmount); } catch (err) { throw new PaymentError('Charge failed', err); } return order; } ``` Splitting into separate `try/catch` blocks gives you different error types per step. One big `try/catch` is fine too when you want uniform handling. ### Express error middleware Express recognizes an error middleware by its 4-argument signature. Place it after all routes: ```js // Wrap async handlers so thrown errors reach next() function asyncHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; } app.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) throw new NotFoundError('User'); res.json(user); })); // Central error handler - must be the LAST middleware app.use((err, req, res, next) => { const status = err.statusCode || 500; res.status(status).json({ error: err.message, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, }); }); ``` Without `asyncHandler`, async errors from route handlers never reach the error middleware. The wrapper calls `.catch(next)`, which hands the error to Express's internal error pipeline. ### Custom error classes Throwing `new Error('not found')` works. But HTTP APIs need status codes, and your error middleware needs to distinguish your own errors from unexpected ones in third-party libraries: ```js class AppError extends Error { constructor(message, statusCode = 500) { super(message); this.statusCode = statusCode; this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); // stack starts at throw site } } class NotFoundError extends AppError { constructor(resource) { super(`${resource} not found`, 404); } } class ValidationError extends AppError { constructor(field, issue) { super(`${field}: ${issue}`, 422); this.field = field; } } ``` The error middleware checks `err instanceof AppError` to separate expected errors from surprises. `Error.captureStackTrace` keeps the stack trace pointing at your throw site, not inside the constructor. ### Global process handlers These catch anything that slipped past every `try/catch` and `.catch()`: ```js // Node.js 15+ crashes by default on unhandled rejections // This handler lets you log before exit process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection:', reason); process.exit(1); }); // Synchronous throws that escaped all try/catch process.on('uncaughtException', (err) => { console.error('Uncaught exception:', err); process.exit(1); // required - process state is undefined after this }); ``` I've seen teams add `process.on('uncaughtException')` thinking it makes the server fault-tolerant. It does not. It hides crashes and runs broken code. Proper `try/catch` coverage plus a process manager that restarts on exit is the actual solution. ### Common mistakes **Swallowing errors in async functions:** ```js // Wrong - caller receives undefined, error disappears async function getUser(id) { try { return await fetchUser(id); } catch (err) { console.error(err); // logs, but doesn't re-throw } } // Correct async function getUser(id) { try { return await fetchUser(id); } catch (err) { console.error(err); throw err; } } ``` **Missing asyncHandler in Express:** ```js // Wrong - rejection is unhandled, never reaches error middleware app.get('/user/:id', async (req, res) => { const user = await User.findById(req.params.id); res.json(user); }); ``` **Using Promise.all() when partial failure is acceptable:** ```js // If any one fails, all fail const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]); // Promise.allSettled() returns results for each, regardless of failure const results = await Promise.allSettled([fetchUsers(), fetchPosts()]); const users = results[0].status === 'fulfilled' ? results[0].value : []; const posts = results[1].status === 'fulfilled' ? results[1].value : []; ``` **Catching too broadly:** ```js // Catches TypeErrors from bugs as well as operational failures try { const data = await fetchData(); processData(data); // a bug here gets swallowed and returns 500 } catch (err) { res.status(500).json({ error: 'Something went wrong' }); } ``` Separate the fetch (which can fail operationally) from the processing (which should not fail if the code is correct). ### Real-world usage - Express APIs: `asyncHandler` wrapper on every route + central error middleware - Database layer: catch specific error codes (e.g. Postgres `23505` for unique violation) and throw `ValidationError` - External API calls: `axios` rejects on non-2xx, wrap in `try/catch` and check `err.response.status` - File operations: check `err.code === 'ENOENT'` to separate missing file from permission error - Parallel data loading: `Promise.allSettled()` when each piece is optional ### Follow-up questions **Q:** What is the difference between `unhandledRejection` and `uncaughtException`? **A:** `unhandledRejection` fires when a rejected Promise has no `.catch()` handler attached. `uncaughtException` fires when a synchronous `throw` escapes all `try/catch` blocks. Both require `process.exit(1)`. **Q:** Why must `uncaughtException` call `process.exit(1)`? **A:** After an uncaught exception, the process state is undefined. Open database connections, in-flight timers, and pending I/O may be in inconsistent states. Continuing to handle requests means serving potentially corrupted data. Exit and restart is always safer. **Q:** How does Express know which middleware handles errors? **A:** Express checks the function's `.length` property. A function with exactly 4 parameters `(err, req, res, next)` is treated as an error handler. When `next(err)` is called, Express skips all normal middlewares until it finds one with 4 parameters. **Q:** What does `Error.captureStackTrace(this, this.constructor)` do? **A:** It sets the stack trace to start at the point where you called `throw`, not inside the `AppError` constructor. Without it, the top of every stack trace shows the constructor, which tells you nothing about where the error originated. **Q:** How do you handle errors from `Promise.all()` where you want partial results? **A:** Replace `Promise.all()` with `Promise.allSettled()`. It always resolves with an array where each item has `status: 'fulfilled'` and `value`, or `status: 'rejected'` and `reason`. Filter by status to extract what succeeded. ## Examples ### Async route with central error handling ```js const express = require('express'); const app = express(); class AppError extends Error { constructor(message, statusCode = 500) { super(message); this.statusCode = statusCode; Error.captureStackTrace(this, this.constructor); } } class NotFoundError extends AppError { constructor(resource) { super(`${resource} not found`, 404); } } function asyncHandler(fn) { return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); } app.get('/orders/:id', asyncHandler(async (req, res) => { const order = await Order.findById(req.params.id); if (!order) throw new NotFoundError('Order'); res.json(order); })); // Placed after all routes app.use((err, req, res, next) => { res.status(err.statusCode || 500).json({ error: err.message }); }); ``` Route handlers stay clean. All error shaping happens in one place. ### Wrapping callback APIs with util.promisify ```js const fs = require('fs'); const { promisify } = require('util'); const readFile = promisify(fs.readFile); async function loadConfig(filePath) { try { const raw = await readFile(filePath, 'utf8'); return JSON.parse(raw); } catch (err) { if (err.code === 'ENOENT') { // operational: file missing, return defaults return { port: 3000, debug: false }; } // unexpected: JSON parse error or permission denied - re-throw throw err; } } ``` Checking `err.code` separates the expected case (file does not exist yet) from a genuine problem (invalid JSON, disk error). ### Promise.allSettled() for a dashboard endpoint ```js async function getDashboardData(userId) { const [ordersResult, profileResult, notificationsResult] = await Promise.allSettled([ fetchOrders(userId), fetchProfile(userId), fetchNotifications(userId), ]); return { orders: ordersResult.status === 'fulfilled' ? ordersResult.value : [], profile: profileResult.status === 'fulfilled' ? profileResult.value : null, notifications: notificationsResult.status === 'fulfilled' ? notificationsResult.value : [], errors: [ordersResult, profileResult, notificationsResult] .filter(r => r.status === 'rejected') .map(r => r.reason?.message), }; } ``` The dashboard loads even when one service is down. Each section either returns real data or a safe fallback, and the `errors` array tells the client which parts failed.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.