Fix UnhandledPromiseRejection: Unhandled promise rejection in async route handler in Express
This error occurs when an async Express route handler throws an error that is not caught. Express does not natively catch promise rejections from async functions, causing the process to crash. Fix it by wrapping async handlers in a try-catch block or using an async error-handling wrapper middleware.
Reading the Stack Trace
Here's what each line means:
- triggerUncaughtException(err, true /* fromPromise */);: Node.js detected an unhandled promise rejection and is about to crash the process.
- at getUsers (/app/src/routes/users.js:12:11): The async getUsers handler threw at line 12 without a try-catch, so the rejection went unhandled.
- at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5): Express router layer does not await async handlers, so thrown errors are not forwarded to error middleware.
Common Causes
1. Async handler without try-catch
The route handler is async but has no try-catch, so any thrown error becomes an unhandled rejection that crashes the server.
router.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
});
2. Missing .catch on promise chain
A promise-based call is made without .catch(), and the promise rejects due to a database or network error.
router.get('/users', (req, res) => {
db.query('SELECT * FROM users').then(users => {
res.json(users);
});
});
The Fix
Wrap every async route handler with an asyncHandler utility that catches rejected promises and forwards them to Express error middleware via next(err). This prevents unhandled rejections and returns proper error responses.
router.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
});
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
router.get('/users', asyncHandler(async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
}));
// Error-handling middleware (in app.js)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
Testing the Fix
const request = require('supertest');
const express = require('express');
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
describe('GET /users', () => {
let app;
beforeEach(() => {
app = express();
});
it('returns 500 when the database query fails', async () => {
app.get('/users', asyncHandler(async (req, res) => {
throw new Error('Database query failed');
}));
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
const response = await request(app).get('/users');
expect(response.status).toBe(500);
expect(response.body.error).toBe('Database query failed');
});
it('returns users on success', async () => {
app.get('/users', asyncHandler(async (req, res) => {
res.json([{ id: 1, name: 'Alice' }]);
}));
const response = await request(app).get('/users');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(1);
});
});
Run your tests:
npm test
Pushing Through CI/CD
git checkout -b fix/unhandled-promise-rejection,git add src/middleware/asyncHandler.js src/routes/users.js,git commit -m "fix: wrap async route handlers to catch promise rejections",git push origin fix/unhandled-promise-rejection
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
- run: npm run lint
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
npm install bugstack-sdk
Step 2: Initialize
const { initBugStack } = require('bugstack-sdk')
initBugStack({ apiKey: process.env.BUGSTACK_API_KEY })
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Run the test suite locally to verify error handling works for all async routes.
- Open a pull request with the asyncHandler wrapper and error middleware.
- Wait for CI checks to pass on the PR.
- Have a teammate review and approve the PR.
- Merge to main and monitor error rates in production to confirm the fix.
Frequently Asked Questions
BugStack tests every async route with both success and failure scenarios, verifies error middleware responds correctly, and ensures no unhandled rejections remain.
All fixes are submitted as pull requests. Your CI pipeline and code review process catch any issues before the fix reaches production.
Express 4.x was designed before async/await was common. Express 5.x will handle async errors natively, but for Express 4.x you need a wrapper.
Yes, the express-async-errors package monkey-patches Express to handle async errors automatically. It's a valid alternative to the manual wrapper approach.