Suggest an editImprove this articleRefine the answer for “How to handle errors in Express.js?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Express.js error handling** works differently for sync and async code. Express catches synchronous errors in route handlers automatically. Async errors require try/catch and `next(err)` to reach error middleware. ```js app.get('/user/:id', async (req, res, next) => { try { const user = await User.findById(req.params.id); res.json(user); } catch (err) { next(err); } }); app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message }); }); ``` **Key:** error middleware needs exactly 4 parameters `(err, req, res, next)` and must be the last middleware registered.Shown above the full answer for quick recall.Answer (EN)Image**Express.js error handling** - a mechanism that catches errors thrown in route handlers and routes them to dedicated error middleware, which sends a formatted response instead of crashing the process. ## Theory ### TL;DR - Synchronous errors in route handlers are caught by Express automatically - Async errors (promises, await) are NOT caught automatically - you must use try/catch and call `next(err)` - Error middleware has exactly 4 parameters `(err, req, res, next)` and must be registered last - The `asyncHandler` wrapper eliminates try/catch boilerplate across all async routes - In Node 15+, an unhandled promise rejection crashes the process ### Quick example ```js // WRONG: async error bypasses Express error handling app.get('/user/:id', async (req, res) => { const user = await User.findById(req.params.id); // rejection is unhandled res.json(user); }); // RIGHT: catch and pass to error middleware app.get('/user/:id', async (req, res, next) => { try { const user = await User.findById(req.params.id); res.json(user); } catch (err) { next(err); // routes to error-handling middleware } }); // Error middleware - 4 params, registered last app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message }); }); ``` Express sees the 4-parameter signature and treats this as an error handler, not regular middleware. ### Sync vs async errors Express wraps synchronous route handlers in an internal try/catch block. When you `throw` inside a sync handler, Express catches it and passes it down the error middleware chain. Async handlers break this. When an `async` function is called, it immediately returns a Promise. Express invokes the handler, gets back a Promise, and moves on. If that Promise rejects later, Express is already done - the rejection happens outside its try/catch scope. Node.js emits an `unhandledRejection` event, which crashes the process in Node 15+. That is the single biggest source of unhandled errors I've seen in Express codebases. One missing try/catch in a high-traffic route, and the server goes down. ### When to use - **Sync code in a route** - throw freely, Express catches it - **async/await in a route** - wrap in try/catch, call `next(err)` in the catch block - **Callbacks (legacy code)** - call `next(err)` in the error path of the callback - **Multiple async routes** - use an `asyncHandler` wrapper to avoid repeating try/catch everywhere - **Unmatched routes (404)** - add a catch-all middleware after all routes, before the error handler ### The asyncHandler pattern Writing try/catch in every route gets repetitive fast. The `asyncHandler` wrapper solves this: ```js // Wrapper catches rejected promises and passes to next() const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; // No try/catch needed in the route itself app.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) { const err = new Error('Not found'); err.status = 404; throw err; // wrapper catches this and calls next(err) } res.json(user); })); ``` The wrapper resolves the returned Promise and, if it rejects, passes the error directly to `next`. This is the same pattern used by `express-async-errors` and `express-async-handler` on npm. ### Custom error classes Attaching a `status` property to a plain `Error` works for small apps. Custom error classes make things more organized at scale: ```js class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.isOperational = true; // flag for expected errors Error.captureStackTrace(this, this.constructor); } } class NotFoundError extends AppError { constructor(resource = 'Resource') { super(`${resource} not found`, 404); } } class ValidationError extends AppError { constructor(message) { super(message, 400); } } ``` In error middleware, check `err instanceof NotFoundError` to handle different error types differently. Operational errors (bad input, missing resource) carry their own status codes. Unexpected errors default to 500. ### How Express identifies error middleware Express checks the `.length` property of a middleware function - the number of declared parameters. Regular middleware has 2 or 3. Error middleware has exactly 4. If you write `(err, req, res)` - 3 params - Express treats it as regular middleware and skips it entirely for errors. That is why `next` must always be declared in error middleware, even if you never call it. ### Common mistakes **Mistake 1: async handler without try/catch** ```js // WRONG: rejection is unhandled, process crashes in Node 15+ app.get('/data', async (req, res) => { const data = await fetchData(); res.json(data); }); // RIGHT app.get('/data', async (req, res, next) => { try { const data = await fetchData(); res.json(data); } catch (err) { next(err); } }); ``` Express only wraps the synchronous part of the handler. The async continuation runs after the handler has already returned. **Mistake 2: error middleware registered before routes** ```js // WRONG: error handler registered first - never reached by route errors app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); app.get('/users', async (req, res, next) => { /* ... */ }); // RIGHT: routes first, error handler last app.get('/users', async (req, res, next) => { /* ... */ }); app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); ``` Express processes middleware in registration order. An error handler registered before routes is never in the chain when those routes execute. **Mistake 3: calling next() before async work** ```js // WRONG app.get('/user/:id', async (req, res, next) => { next(); // tells Express "done here" const user = await User.findById(req.params.id); // error here has no handler res.json(user); }); ``` Once `next()` is called without an error argument, Express moves to the next middleware. Any error that happens after that has no handler waiting for it. **Mistake 4: sending response and passing error** ```js // WRONG: both res.json() and next(err) can fire app.get('/user/:id', async (req, res, next) => { const user = await User.findById(req.params.id).catch(next); res.json(user); // runs even if .catch(next) already fired }); // RIGHT: use try/catch and do one or the other app.get('/user/:id', async (req, res, next) => { try { const user = await User.findById(req.params.id); if (!user) throw new NotFoundError('User'); res.json(user); } catch (err) { next(err); } }); ``` Once `res.json()` or `res.send()` is called, the response is sent. Calling it again produces a "Cannot set headers after they are sent" error. **Mistake 5: treating all errors the same** ```js // WRONG: everything becomes a 500 app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); // RIGHT: use status from the error object app.use((err, req, res, next) => { const status = err.statusCode || err.status || 500; const message = err.message || 'Internal server error'; if (!err.isOperational) { console.error('Unexpected error:', err); // log programmer errors } res.status(status).json({ error: { status, message }, ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }); ``` Operational errors (NotFoundError, ValidationError) carry their own status codes and are expected. Programmer errors should be logged and return 500. Treating them the same hides real bugs. ### Real-world usage - **Express apps** - async routes need try/catch + `next(err)` or an asyncHandler wrapper on every handler - **express-async-errors** - npm package that patches Express to handle async errors automatically, no wrapper needed - **NestJS** - uses exception filters instead of middleware, same concept with different syntax - **Fastify** - built-in async error handling, no wrapper required - **Koa** - uses try/catch in middleware directly, no `next(err)` pattern ### Follow-up questions **Q:** Why doesn't Express catch errors in async handlers automatically? **A:** Express wraps the synchronous part of the handler in try/catch. An async function returns a Promise immediately, so by the time the Promise rejects, the handler has already returned and the try/catch is gone. The rejection happens outside Express's scope entirely. **Q:** What happens to unhandled promise rejections in Node 15+? **A:** Node.js terminates the process. Before Node 15 it only emitted a deprecation warning. This is why an uncaught async error in Express is not just bad practice - it takes down production. **Q:** Can you have multiple error-handling middleware functions? **A:** Yes. Express calls them in order until one stops calling `next(err)`. You can have specialized handlers for validation and database errors before a catch-all handler. Each must declare exactly 4 parameters. **Q:** What happens if error middleware itself throws? **A:** Express catches it and sends a generic 500 response. Error middleware should be defensive: check that `err` has expected properties, use defaults, never call external code without protection. **Q:** How would you design error handling for a large app with multiple domains (auth, users, payments)? **A:** Create domain-specific error classes - `AuthenticationError`, `PaymentError`, `ValidationError` - each with a `statusCode` property. Throw the right class in route handlers. In the global error handler, check `instanceof` to format responses differently: `AuthenticationError` returns 401 with no internal detail, `ValidationError` returns 400 with field-level messages, unexpected errors return 500 with a log entry. This separates formatting logic from business logic and makes every error path independently testable. ## Examples ### Basic: sync vs async error handling ```js const express = require('express'); const app = express(); // Sync: Express catches this automatically app.get('/sync', (req, res) => { throw new Error('Something went wrong'); // no try/catch needed }); // Async: must catch manually and pass to next app.get('/async', async (req, res, next) => { try { const data = await fetchSomething(); res.json(data); } catch (err) { next(err); // passes error to error middleware } }); // Error middleware - must be last, must have exactly 4 params app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message }); }); app.listen(3000); ``` A sync `throw` goes straight to error middleware. An async error without try/catch crashes the process in Node 15+. ### Intermediate: production route with validation and database errors ```js app.post('/users', async (req, res, next) => { try { // operational error - bad input from client if (!req.body.email) { const err = new Error('Email is required'); err.status = 400; throw err; } const user = await User.create(req.body); res.status(201).json(user); } catch (err) { next(err); // all errors flow to the error handler } }); app.use((err, req, res, next) => { const status = err.status || 500; const message = err.message || 'Internal server error'; res.status(status).json({ error: { status, message }, // stack visible in development only ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }); ``` Validation errors return 400. Unexpected database errors return 500. Stack trace appears only in development. ### Advanced: full setup with custom error classes, asyncHandler, and 404 handling ```js // Custom error classes class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.isOperational = true; Error.captureStackTrace(this, this.constructor); } } class NotFoundError extends AppError { constructor(resource = 'Resource') { super(`${resource} not found`, 404); } } class ValidationError extends AppError { constructor(message) { super(message, 400); } } // asyncHandler wrapper - no try/catch in routes const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); // Route with no boilerplate app.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) throw new NotFoundError('User'); // wrapper passes to next() res.json(user); })); // 404 catch-all - after all routes, before error handler app.use((req, res, next) => { next(new NotFoundError(`Route ${req.method} ${req.path}`)); }); // Global error handler - always last app.use((err, req, res, next) => { const status = err.statusCode || err.status || 500; if (!err.isOperational) { console.error('Unexpected error:', err); // log programmer errors } res.status(status).json({ error: err.message, ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }) }); }); ``` `isOperational` marks errors you expect (bad input, missing records). Unexpected errors get logged with full stack before a generic 500 goes back to the client.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.