How It Works Features Pricing Blog
Log In Start Free Trial
Express · JavaScript

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

node:internal/process/promises:289 triggerUncaughtException(err, true /* fromPromise */); ^ [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(). The promise rejected with the reason "Error: Database query failed".] { code: 'ERR_UNHANDLED_REJECTION' } Error: Database query failed at getUsers (/app/src/routes/users.js:12:11) at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at next (/app/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/app/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at /app/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12) at next (/app/node_modules/express/lib/router/index.js:280:10)

Here's what each line means:

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.

Before (broken)
router.get('/users', async (req, res) => {
  const users = await db.query('SELECT * FROM users');
  res.json(users);
});
After (fixed)
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:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. 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:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. 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)

  1. Run the test suite locally to verify error handling works for all async routes.
  2. Open a pull request with the asyncHandler wrapper and error middleware.
  3. Wait for CI checks to pass on the PR.
  4. Have a teammate review and approve the PR.
  5. 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.