Uncaught (in promise) is JavaScript's way of telling you a promise was rejected but nobody caught it. Unlike a regular thrown error that stops execution, a rejected promise with no .catch() handler silently dies — leaving your app in a bad state while you remain oblivious until a user hits the broken flow. This guide explains what the error means, why it happens, and how to track it down and fix it in production.
The phrase "uncaught in promise" appears in error trackers and browser consoles when an async operation fails and there's no error handler waiting for it. It's one of the most common and hardest-to-diagnose JavaScript errors because the stack trace often points at the wrong place, the rejection can happen milliseconds after the promise is created, and it won't stop your code — it'll just silently break a feature.
What 'Uncaught (in promise)' actually means
A promise rejection happens when you explicitly call reject(), throw an error inside a promise, or an async operation fails:
const promise = new Promise((resolve, reject) => {
reject(new Error("Something went wrong"));
});
// If nothing catches this rejection, you get 'Uncaught (in promise)'
The browser detects the rejection and fires an unhandledrejection event. If your app isn't listening for that event, the error gets logged and lost — no thrown exception, no console error you can step through, just a silent failure that corrupts your app's state.
A promise rejection is not the same as a thrown error. Thrown errors stop code execution immediately. Unhandled rejections let your code keep running, which can be worse — downstream code may depend on data that never arrived.
Why it's different from synchronous errors
JavaScript's error model has two parallel tracks: synchronous errors (thrown exceptions) and asynchronous errors (rejected promises). Your window.onerror handler catches thrown errors. Your unhandledrejection event handler catches promise rejections. If you've set up error tracking but ignored promise rejections, you're only seeing half the failures.
Most teams set up global error handlers for uncaught exceptions but forget about rejections — and then wonder why the error dashboard misses huge chunks of production failures. The browser won't show you an error dialog for unhandled rejections; it just logs to the console and keeps running. By the time a user reports a broken feature, the promise rejection happened hours ago.
The most common causes
Missing .catch() on a fetch or async call
The most frequent culprit is forgotten error handling on a network request:
// This rejects if the network fails or the server returns an error
fetch("/api/data")
.then(res => res.json())
.then(data => setState(data));
// No .catch() — rejection is uncaught
The fix is simple: add .catch():
fetch("/api/data")
.then(res => res.json())
.then(data => setState(data))
.catch(error => {
console.error("Failed to fetch data:", error);
setState(null); // or show an error UI
});
Or with async/await, use try/catch:
async function loadData() {
try {
const res = await fetch("/api/data");
const data = await res.json();
setState(data);
} catch (error) {
console.error("Failed to fetch data:", error);
setState(null);
}
}
Async handlers in event listeners
A common trap is attaching an async handler to a click or form event without catching rejections inside it:
button.addEventListener("click", async () => {
const response = await fetch("/api/purchase");
// If fetch fails, the rejection escapes and is uncaught
});
The fix is to wrap the async logic in try/catch:
button.addEventListener("click", async () => {
try {
const response = await fetch("/api/purchase");
const result = await response.json();
// handle success
} catch (error) {
// handle error, show toast, etc.
}
});
Chained .then() calls without a final .catch()
Each .then() can throw or return a rejected promise, and if any rejection in the chain goes uncaught, the browser logs it:
promise
.then(x => processX(x)) // might reject
.then(y => processY(y)) // might reject
// No .catch() at the end — any rejection is uncaught
Always chain .catch() at the end:
promise
.then(x => processX(x))
.then(y => processY(y))
.catch(error => {
console.error("Pipeline failed:", error);
// recover gracefully
});
When using async/await, wrap the whole function in try/catch. When chaining .then(), always end with .catch(). Both patterns catch all rejections in the chain.
Finding it in production
In production, a bare stack trace from an "uncaught in promise" error is usually unhelpful — it points at the promise microtask queue, not the code that created the promise. That's where JavaScript error monitoring becomes essential: the error tracker records the breadcrumbs that led to the rejection — the user action, the API call, the page navigation — so you can see what the user was doing when the promise failed.
When you see an uncaught promise rejection in your error tracker, the stack trace may only show the rejection point, but the breadcrumb trail shows what happened before: "User clicked 'place order'" → "POST /api/purchase" → "Uncaught (in promise)". From there you can often guess whether the error was a missing .catch() or a genuine API failure.
For the full detective work — tracing back to the root cause through stack frames and error context — see the guide to debugging production errors. Once you've fixed the issue, an error triage process keeps rejections from piling up again.
Preventing rejections with global handlers
Beyond fixing individual .then() chains, you should set up a global unhandledrejection listener to catch any rejection that escapes your explicit error handling:
window.addEventListener("unhandledrejection", event => {
console.error("Unhandled rejection:", event.reason);
// Report to error tracker or show user feedback
event.preventDefault(); // Prevents the default browser behavior
});
This catches rejections that your code missed, and it gives you a chance to send them to your error tracker (like LightTrace) so you're not flying blind. Most error tracking SDKs set this up automatically:
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "https://<key>@your-lighttrace-host/1",
// Sentry automatically listens for unhandledrejection
});
Once that's in place, every uncaught promise rejection gets captured, grouped, and sent to your dashboard — no more silent failures.
Testing promise rejections locally
Before shipping, test that your error handlers actually run. A common mistake is writing a .catch() that doesn't report the error, just silently swallows it:
fetch("/api/data")
.catch(() => {}); // Silent failure — no logging, no recovery
Instead, log the error or recover with fallback data:
fetch("/api/data")
.catch(error => {
console.error("Fetch failed:", error);
return { data: [] }; // Fallback
});
In development, add intentional failures to verify your error handlers run. Disable your network in DevTools, or mock an API to reject, and confirm your .catch() or try/catch block executes as expected.
Start tracking errors in minutes
Stop chasing silent promise rejections. Set up global error tracking with LightTrace to capture every unhandled rejection, see the breadcrumbs that led there, and fix the bug before your users report it.
Understanding promise rejections and catching them consistently turns one of JavaScript's most frustrating failure modes into a solvable problem. For a broader look at handling all async errors in the browser, see global JavaScript error handling, and for a deeper dive into the rejection event itself, read about unhandled promise rejections.