Diego Betto
Photo by David Dintsh

Diego Betto · September 17, 2026 · 4 min di lettura

"Uncaught (in promise)": why your async error disappears from the console

The difference between a thrown exception and an unhandled promise rejection, why a surrounding try/catch sometimes doesn't help, and how to catch async errors correctly.

Condividi:XLinkedInFacebookWhatsApp

Something fails somewhere in your async code, and instead of a clean stack trace you get this in the console:

⛔ Error

Uncaught (in promise) TypeError: Failed to fetch

It’s not a bug in the browser and it’s not a different kind of error — it’s the exact same TypeError you’d get synchronously, just reported differently because nothing was listening for it. Understanding why that happens is the key to fixing it reliably.

What “unhandled rejection” actually means

A Promise can settle in one of two ways: it resolves, or it rejects. If code calls .catch() (or an await inside a try/catch) on that promise, the rejection is “handled.” If nothing does, by the time the JavaScript engine notices the promise was rejected and nobody ever attached a handler, it reports it as an unhandled rejection — that’s exactly what “Uncaught (in promise)” means.

fetch("/api/data"); // no .then, no .catch — if this rejects, nothing catches it

This is different from a plain synchronous throw, which crashes immediately at the point it happens. A promise rejection can sit “unhandled” for a while — the engine has to wait a tick to see whether you were ever going to add a .catch() — which is part of why it shows up as its own distinct kind of error in the console.

Why a try/catch around the call sometimes doesn’t help

This is the part that trips people up the most:

try {
  fetchData(); // fetchData is an async function, called without await
} catch (error) {
  console.error(error); // never runs
}

async function fetchData() {
  const res = await fetch("/api/data");
  return res.json();
}

fetchData() returns a promise immediately — the try/catch around it only catches errors thrown synchronously while calling fetchData, not errors that happen later, asynchronously, inside it. Without await in front of the call, the try block has already finished by the time the promise could reject.

try {
  await fetchData(); // now the try/catch actually waits for it and can catch the rejection
} catch (error) {
  console.error(error);
}

The rule to remember: try/catch only catches a promise’s rejection if you await that promise (or otherwise chain a .catch() onto it) — wrapping the call alone isn’t enough.

Catching rejections with .then()/.catch()

If you’re not using async/await, the equivalent is attaching .catch() directly to the promise chain:

fetchData()
  .then((data) => render(data))
  .catch((error) => console.error("Failed to load data:", error));

A .catch() at the end of the chain catches a rejection from any step before it — the initial fetchData() call or any .then() in between — which is one reason chained .then()/.catch() is often easier to reason about than several separate try/catch blocks scattered through async code.

The global safety net: unhandledrejection

For cases you genuinely can’t predict (a fire-and-forget async call somewhere deep in a library, for example), the browser gives you a global event as a last resort:

window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled promise rejection:", event.reason);
  // event.preventDefault() stops it from also logging to the console as an error
});

⚠ Not a substitute for handling errors where they happen

Treat unhandledrejection as a safety net for logging/monitoring (Sentry and similar tools hook into it), not as your primary error-handling strategy. Catching errors close to where they occur — with a specific .catch() or try/catch around the await — is what lets you actually recover gracefully (show a retry button, fall back to cached data) instead of just knowing something broke.

FAQ

❓ Does this only happen with fetch?

No — any rejected promise without a handler triggers it, whether the rejection comes from fetch, a database call, Promise.reject() directly, or an async function that throws. fetch shows up often simply because network calls are a common source of real-world failures.

❓ Will an unhandled rejection crash my app?

In the browser, no — it’s logged as an error but execution continues. In Node.js, since v15, unhandled promise rejections terminate the process by default (a deliberate change from earlier versions, which only warned), so it’s worth explicitly handling them in server code rather than relying on the process staying alive.

❓ Is AbortController related to this?

Related but separate — aborting a fetch via AbortController causes the promise to reject with an AbortError, which still needs a .catch()/try-catch like any other rejection, or you’ll see this exact warning every time you cancel a request. I cover the full pattern in my AbortController article.

Conclusion

“Uncaught (in promise)” isn’t a special category of bug — it’s the console telling you a rejected promise never got a .catch() or an await inside a try/catch. The fix is always the same: make sure every promise you create or call is either awaited inside a try/catch, chained with .catch(), or deliberately monitored globally as a last resort, not left to fail silently.

References

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.