Fix UnhandledPromiseRejectionWarning in Node.js async/await

intermediate๐Ÿ’š Node.js2026-03-19| Node.js v10โ€“v18+, all OS (Linux, macOS, Windows)

Error Message

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1)
#nodejs#promise#async#await#error-handling

What happened

Your Node.js app spits out something like this in the console:

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1)
UnhandledPromiseRejectionWarning: Error: connect ECONNREFUSED 127.0.0.1:5432
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16)
(node:12345) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated.
In future versions of Node.js, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Something silently failed โ€” a DB connection, an API call, a file read. The app keeps running like nothing happened. In Node.js v15+, this actually crashes the process outright. In older versions it just prints this warning and moves on, which is arguably worse. Silent failures are the hardest bugs to track down in production.

Why this happens

A Promise rejected somewhere in your code but nothing caught it. Three patterns cause this almost every time:

  • An async function threw an error but the caller didn't await it inside a try/catch
  • A .then() chain has no .catch() at the end
  • An event handler or callback called an async function but ignored the returned Promise

Finding the source

Start by getting a useful stack trace. Run Node with the --trace-warnings flag:

node --trace-warnings app.js

This prints the full stack trace pointing to the exact line where the rejection originated. Without it, older Node versions only show the rejection message โ€” you get no clue where it came from.

Another option: drop a global handler at the top of your entry file while debugging:

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason);
});

Put it at line 1 of index.js or wherever your app starts. It catches every rejection that slips through. Useful for hunting down the problem โ€” just don't treat it as a permanent fix.

The fixes

Fix 1: Wrap async/await in try/catch

Nine times out of ten, the culprit looks like this โ€” an async function called without any error handling:

// Broken โ€” rejection goes unhandled
async function fetchUser(id) {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
  return user;
}

fetchUser(123); // No await, no .catch()

Wrap the internals with try/catch, then handle at the call site too:

// Fixed โ€” errors are caught
async function fetchUser(id) {
  try {
    const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
    return user;
  } catch (err) {
    console.error('Failed to fetch user:', err.message);
    throw err; // Re-throw so the caller knows something went wrong
  }
}

// Handle at call site
try {
  const user = await fetchUser(123);
} catch (err) {
  // Handle or log
}

Fix 2: Add .catch() to Promise chains

Using .then() chains instead of async/await? Every chain needs a .catch() at the end:

// Broken โ€” rejection silently disappears
fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => process(data));
  // Missing .catch()
// Fixed
fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => process(data))
  .catch(err => {
    console.error('API call failed:', err.message);
  });

Fix 3: Handle async functions inside event handlers

This one catches people off guard. Event emitters and callbacks don't understand Promises, so errors inside async callbacks silently escape:

// Broken โ€” Express route
app.get('/users/:id', async (req, res) => {
  const user = await fetchUser(req.params.id); // throws? Express never sees it
  res.json(user);
});
// Fixed โ€” wrap with try/catch and forward to next()
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await fetchUser(req.params.id);
    res.json(user);
  } catch (err) {
    next(err); // Pass to Express error handler
  }
});

Writing this boilerplate on every route gets old fast. A small wrapper utility cleans it up:

const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await fetchUser(req.params.id);
  res.json(user);
}));

Fix 4: Global handler as a safety net (production)

Even with solid error handling throughout your codebase, a global fallback is worth adding. It's the last line of defense:

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Promise Rejection:', reason);
  // Log to your monitoring service (Sentry, Datadog, etc.)
  // then optionally shut down gracefully
  // process.exit(1);
});

On Node.js v15+, the process already exits on unhandled rejections by default. This handler gives you a window to log the error and run cleanup before that happens.

Verify the fix

Run your app and deliberately trigger the code path that caused the warning:

node --trace-warnings app.js
  • The UnhandledPromiseRejectionWarning line should be gone
  • If the error condition still fires, you should now see a proper caught error in the logs instead
  • On Node.js v15+, your app should no longer crash unexpectedly

Write a quick test to force a rejection and confirm it's handled:

// Force a rejection to verify your handler works
async function test() {
  try {
    await Promise.reject(new Error('test rejection'));
  } catch (err) {
    console.log('Caught expected error:', err.message); // This line should print
  }
}

test();

Lessons learned

Every async function call needs error handling โ€” full stop. Either try/catch around the await, or .catch() on the returned Promise. The tricky spots are async functions called from non-async contexts: event handlers, setTimeout callbacks, stream listeners. None of those care about your rejected Promises.

Running Node.js v14 or older? Don't shrug off these warnings just because the process keeps running. Upgrade to v15+ and Node will force your hand โ€” better to fix it now than trace a silent data corruption bug at 2am in production.

Keep --trace-warnings in your debug toolkit. The moment you see a warning with no clear source, that flag will find it in seconds.

Related Error Notes