Introduction
Express 4.x does not automatically catch rejected promises from async route handlers. When an async handler throws, the rejection is unhandled, causing an unhandledRejection event and (since Node.js v15) process termination. This means any database error, API call failure, or validation error in an async route crashes the entire server.
Symptoms
- Server crashes with
UnhandledPromiseRejectionon any route error Error: connect ECONNREFUSEDin route handler kills the serverunhandledRejectionwarning followed by process exit- Single bad request takes down the entire application
- Error does not reach Express error middleware
``` (node:12345) UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().
Error: User not found at UserController.findById (/app/controllers/user.js:15:11) at async /app/routes/user.js:8:18
Node.js v20.11.0 # Process exits - server is down! ```
Common Causes
- Async route handler without try/catch
awaitthat throws without error handling- Express 4.x not supporting async handler error catching
- Database query errors not caught in route handlers
- Validation errors thrown as exceptions
Step-by-Step Fix
- 1.Use async handler wrapper:
- 2.```javascript
- 3.// Create a wrapper that catches async errors
- 4.const asyncHandler = (fn) => (req, res, next) => {
- 5.Promise.resolve(fn(req, res, next)).catch(next);
- 6.};
// Use it on all async routes app.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) { const error = new Error('User not found'); error.statusCode = 404; throw error; } res.json(user); }));
// Error goes to Express error middleware via next() ```
- 1.Install express-async-errors for automatic handling:
- 2.```bash
- 3.npm install express-async-errors
- 4.
` - 5.```javascript
- 6.// Require at the top of your entry file (after express)
- 7.const express = require('express');
- 8.require('express-async-errors');
const app = express();
// Now async errors are automatically caught and passed to next() app.get('/users/:id', async (req, res) => { const user = await User.findById(req.params.id); if (!user) { const error = new Error('User not found'); error.statusCode = 404; throw error; // Automatically caught! } res.json(user); });
// Errors reach your error middleware app.use((err, req, res, next) => { const status = err.statusCode || 500; res.status(status).json({ error: err.message }); }); ```
- 1.Create proper Express error middleware:
- 2.```javascript
- 3.// 404 handler - must be after all routes
- 4.app.use((req, res, next) => {
- 5.const error = new Error(
Not Found - ${req.originalUrl}); - 6.error.statusCode = 404;
- 7.next(error);
- 8.});
// Global error handler - must have 4 parameters (err, req, res, next) app.use((err, req, res, next) => { const statusCode = err.statusCode || 500;
// Log full error in development if (process.env.NODE_ENV === 'development') { console.error('Error:', err); }
res.status(statusCode).json({ error: process.env.NODE_ENV === 'production' ? 'Internal Server Error' : err.message, ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), }); }); ```
- 1.Use express.Router with async wrapper:
- 2.```javascript
- 3.const express = require('express');
- 4.const router = express.Router();
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
router.get('/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); res.json(user); }));
router.post('/', asyncHandler(async (req, res) => { const user = await User.create(req.body); res.status(201).json(user); }));
router.put('/:id', asyncHandler(async (req, res) => { const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true, }); res.json(user); }));
module.exports = router; ```
Prevention
- Always wrap async route handlers or use
express-async-errors - Create a centralized error middleware with consistent error responses
- Use custom error classes with
statusCodeproperty - Log errors with full stack traces in development
- Never expose internal error details in production responses
- Add request ID to error logs for tracing
- Test error paths in integration tests with mock failures