Skip to content

Fix: UnhandledPromiseRejectionWarning / UnhandledPromiseRejection

FixDevs ·

Quick Answer

How to fix UnhandledPromiseRejectionWarning in Node.js and unhandled promise rejection errors in JavaScript caused by missing catch handlers, async/await mistakes, and event emitter errors.

The Error

You run a Node.js script or browser app and see:

UnhandledPromiseRejectionWarning: Error: connect ECONNREFUSED 127.0.0.1:5432
UnhandledPromiseRejectionWarning: Unhandled promise rejection. 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().

Or in newer Node.js versions (v15+):

node:internal/process/promises:279
            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: connect ECONNREFUSED]

Or in the browser console:

Uncaught (in promise) TypeError: Failed to fetch
Uncaught (in promise) Error: Request failed with status code 401

The error means a Promise rejected (failed) but nothing handled that rejection — there was no .catch(), no try/catch around await, and no rejection handler attached to the Promise.

Note: In Node.js v15+, an unhandled promise rejection crashes the process by default. In older versions it printed a warning but kept running. If your process is crashing unexpectedly after an upgrade, this is a likely cause.

Why This Happens

Every Promise can be in one of three states: pending, fulfilled, or rejected. When a Promise rejects — a network request fails, a database query errors, or you explicitly call reject() — Node.js or the browser looks for a rejection handler.

If none is found, the runtime emits unhandledRejection. Common causes:

  • Missing .catch() on a promise chain.
  • await without try/catch inside an async function.
  • Async function called without await — the returned promise is ignored.
  • Event handlers that are async — errors thrown inside are not caught by the caller.
  • Promise.all() with no error handler — one rejection kills all.
  • Forgetting to return a promise inside .then(), breaking the chain.

Fix 1: Add .catch() to Promise Chains

Every .then() chain needs a .catch() at the end:

Broken:

fetch("/api/data")
  .then(res => res.json())
  .then(data => console.log(data));
// If fetch fails, the rejection is unhandled

Fixed:

fetch("/api/data")
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error("Fetch failed:", err));

The .catch() must be at the end of the chain. Adding it only to the first .then() will not catch errors thrown in later .then() callbacks.

Return promises inside .then() to keep the chain intact:

// Broken — inner promise is orphaned
fetch("/api/users")
  .then(res => {
    fetch("/api/posts"); // Missing return — this promise is orphaned
  })
  .catch(err => console.error(err)); // Does NOT catch errors from /api/posts

// Fixed
fetch("/api/users")
  .then(res => {
    return fetch("/api/posts"); // Returning keeps it in the chain
  })
  .catch(err => console.error(err));

Fix 2: Wrap await in try/catch

When using async/await, wrap calls that can fail in try/catch:

Broken:

async function loadUser(id) {
  const res = await fetch(`/api/users/${id}`); // Can reject
  const user = await res.json();               // Can also throw
  return user;
}

loadUser(123); // No await, no catch — rejection goes unhandled

Fixed:

async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const user = await res.json();
    return user;
  } catch (err) {
    console.error("Failed to load user:", err);
    throw err; // Re-throw if the caller needs to handle it
  }
}

// Always await or catch the async function's result
try {
  const user = await loadUser(123);
} catch (err) {
  // Handle here
}

Pro Tip: A try/catch around await catches both Promise rejections and synchronous errors thrown inside the async function. It replaces both .catch() and a regular try/catch in most cases.

Fix 3: Always await or catch async function calls

When you call an async function, it returns a Promise. If you do not await it or chain .catch(), any rejection is unhandled:

Broken:

async function sendEmail(to, subject) {
  const result = await emailService.send(to, subject);
  return result;
}

// In an event handler or fire-and-forget call:
button.addEventListener("click", () => {
  sendEmail("[email protected]", "Welcome"); // Missing await AND .catch()
});

Fixed — add .catch():

button.addEventListener("click", () => {
  sendEmail("[email protected]", "Welcome")
    .catch(err => console.error("Email failed:", err));
});

Fixed — make the handler async:

button.addEventListener("click", async () => {
  try {
    await sendEmail("[email protected]", "Welcome");
  } catch (err) {
    console.error("Email failed:", err);
  }
});

Note: Express.js route handlers do not automatically catch async errors. Wrap them or use an async wrapper utility:

// Broken — Express doesn't catch async rejections automatically (Express 4)
app.get("/users", async (req, res) => {
  const users = await db.getUsers(); // If this rejects, Express hangs
  res.json(users);
});

// Fixed — wrap with try/catch
app.get("/users", async (req, res, next) => {
  try {
    const users = await db.getUsers();
    res.json(users);
  } catch (err) {
    next(err); // Pass to Express error handler
  }
});

Express 5 (currently in beta) handles async errors automatically.

Fix 4: Handle Promise.all() Rejections

Promise.all() rejects as soon as any single Promise in the array rejects:

Broken:

async function loadDashboard() {
  const [users, posts, stats] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchStats(), // If this rejects, the whole Promise.all rejects
  ]);
}

loadDashboard(); // No catch

Fixed:

async function loadDashboard() {
  try {
    const [users, posts, stats] = await Promise.all([
      fetchUsers(),
      fetchPosts(),
      fetchStats(),
    ]);
    return { users, posts, stats };
  } catch (err) {
    console.error("Dashboard load failed:", err);
    throw err;
  }
}

If you want all results even if some fail, use Promise.allSettled():

const results = await Promise.allSettled([fetchUsers(), fetchPosts(), fetchStats()]);

results.forEach((result, i) => {
  if (result.status === "fulfilled") {
    console.log(`Request ${i} succeeded:`, result.value);
  } else {
    console.error(`Request ${i} failed:`, result.reason);
  }
});

Why this matters: Promise.allSettled() always resolves with an array of outcome objects — it never rejects. Use it when partial failures are acceptable and you want all available data.

Fix 5: Add a Global Unhandled Rejection Handler

As a safety net — not a replacement for proper error handling — add a global handler to catch any rejections that slip through:

Node.js:

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason);
  // Log to error tracking service (Sentry, Datadog, etc.)
  // Optionally exit: process.exit(1);
});

Browser:

window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled promise rejection:", event.reason);
  event.preventDefault(); // Suppress the browser's default console error
});

Add this near the top of your entry file. It is useful for catching rejections you may have missed, but the root fix is always to add proper .catch() or try/catch to the originating code.

Fix 6: Fix Async Event Emitter Errors

EventEmitter callbacks in Node.js do not automatically propagate errors from async handlers:

Broken:

const EventEmitter = require("events");
const emitter = new EventEmitter();

emitter.on("data", async (data) => {
  await processData(data); // If this rejects, the error is unhandled
});

Fixed:

emitter.on("data", async (data) => {
  try {
    await processData(data);
  } catch (err) {
    emitter.emit("error", err); // Route to the error event
  }
});

emitter.on("error", (err) => {
  console.error("Emitter error:", err);
});

Always add an "error" listener to EventEmitters. An unhandled "error" event in Node.js throws an exception.

Fix 7: Find All Unhandled Rejections in Your Codebase

Search your code for async patterns that lack error handling:

# Find async functions that might be called without await or catch
grep -rn "async function\|async (" src/ --include="*.js" --include="*.ts"

# Find .then() calls without a following .catch()
grep -rn "\.then(" src/ --include="*.js" | grep -v "\.catch("

Use ESLint rules to enforce promise handling automatically:

{
  "rules": {
    "no-floating-promises": "error",
    "@typescript-eslint/no-floating-promises": "error",
    "promise/catch-or-return": "error"
  }
}

The @typescript-eslint/no-floating-promises rule catches unhandled async calls at lint time, before they become runtime errors. For related ESLint issues, see Fix: ESLint Parsing Error: Unexpected token.

Still Not Working?

Check for rejection in a constructor. You cannot use await in a constructor. If you call an async function inside a constructor without handling its result, you get an unhandled rejection. Move async initialization to a static factory method or an init() method called after construction.

Check third-party library callbacks. Some libraries accept callbacks that they call synchronously or asynchronously. If you pass an async callback and the library does not handle the returned Promise, rejections go unhandled. Wrap the callback body in try/catch and handle errors manually.

Check for race conditions with setTimeout or setInterval. Async functions called inside timers run outside any surrounding try/catch. Each timer callback needs its own error handling.

Node.js version matters. Node.js v14 and earlier printed a deprecation warning for unhandled rejections but continued running. Node.js v15+ crashes the process. If your app started crashing after a Node.js upgrade, search for all promise chains that lack .catch(). See Fix: node: cannot find module for other Node.js startup errors.

Use async stack traces. In Node.js, set --async-stack-traces (enabled by default in v12+) or in Chrome DevTools enable “Async” in the Call Stack panel. This shows you where the Promise was created, not just where it rejected — much easier to trace the origin.

For errors thrown inside .then() callbacks that look like TypeError: x is not a function, the unhandled rejection wraps the underlying TypeError — fix the inner error first.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles