You open the browser console, and there it is: Unhandled Promise Rejection with no file name, no line number, and no stack trace. The error message itself is generic, something like Error: something went wrong, and you have zero context. Was it a failed API call? A misconfigured environment variable? A race condition? Without a stack trace, you are hunting blind. This is one of the most frustrating scenarios in modern JavaScript development. The promise rejected, but the error object lost its birth certificate. Let’s fix that.
An uncaught promise error without a stack trace usually means the error was created before it was thrown, or it was a non-Error value. To get the missing trace, wrap rejections in new Error(), attach .catch() at every branch, and use a global unhandledrejection listener. Enable async stack traces in modern devtools and always test with source maps enabled. Consistent error object creation is your best defense.
Why Stack Traces Disappear in Promise Rejections
JavaScript promises run in the microtask queue. When a promise rejects, the error object is created at the moment reject() is called, not at the line that triggered it. If that reject() call passes a string or a number, there is no stack property at all. Even if you pass an Error object, the stack trace captures the call stack at the point of creation, not at the point where the promise was instantiated or where the .then() chain started.
Consider this common pattern:
function fetchData(id) {
return new Promise((resolve, reject) => {
if (!id) {
reject('Missing ID'); // string, no stack
}
// ...
});
}
The rejection here produces an Uncaught (in promise) Missing ID message. Zero stack information. The browser cannot generate a stack trace because there is no Error object to attach it to.
Another common scenario is when the error is thrown inside a setTimeout or setInterval inside a promise. The stack trace at that point points to the timer callback, not the original promise chain. The connection is broken.
The Common Culprits Behind Missing Stack Traces
Before we get into fixes, let’s list the usual suspects. If you see an uncaught promise error no stack trace, check these first:
- Passing a string, number, or
undefinedtoreject()instead of anErrorinstance. - Throwing a non-Error value (e.g.,
throw 42) inside anasyncfunction. - Using
.then()without a.catch()and relying on the global handler, which often loses context. - Errors that originate inside a microtask or a timer inside a promise.
- Minified code in production where source maps are not loaded.
- Using
Promise.reject()at the top level of a module before any promise chain exists. - A
.catch()that re-throws a new error but fails to include the original stack usingError.captureStackTrace(Node.js) or a custom stack chain.
How to Capture the Stack Trace: A Numbered Process
Follow these steps to surface the missing trace. Each step builds on the last.
-
Add a global
unhandledrejectionevent listener. This catches every unhandled promise rejection in the window. Inside the handler, log the promise and the reason. In modern browsers, thepromiseproperty of the event gives you the rejected promise object. You can inspect it in the console to see its internal slots, though the stack trace may still be absent. The real benefit is that you can then call.catch()on the promise right there and break on the rejection. -
Attach a
.catch()to every promise chain that does not return a promise. If you chain.then().then(), the last.then()should have a.catch(). Even if you plan to let errors propagate up, add a.catch()that re-throws a newErrorobject that includes the original reason. This preserves the trace. -
Wrap every promise rejection in
new Error()or a custom error constructor. Never pass raw strings. Always doreject(new Error('descriptive message')). Forasyncfunctions, always throw anErrorobject. This is the single most effective habit. -
Use
async/awaitwith try/catch blocks. Inside anasyncfunction, the stack trace includes the function name and the location of theawaitthat caused the error. This works because the engine creates a new async context that chains the stack. -
Set a breakpoint in the
unhandledrejectionhandler and inspect the call stack in DevTools. Today’s Chrome DevTools have an “async” checkbox in the Call Stack panel. Enable it to see the asynchronous chain leading up to the rejection. Edge and Firefox have similar features.
Techniques Comparison Table
| Technique | Impact on Stack Trace | Effort to Implement | Best For |
|---|---|---|---|
Global unhandledrejection listener |
Low (no chain context) | Low | Quick debugging / prod logging |
.catch() with re-throw |
Medium (preserves current chain) | Medium | Long promise chains |
Always use new Error() in reject |
High (full capture at source) | Low | All code bases |
| Async/await + try/catch | High (includes async context) | Medium | New code / refactors |
| Enable async stack traces in DevTools | High (visual trace) | Zero | Local development |
Using a Global Handler to Surface the Error
Set up a listener at the start of your application, before any promises are created. In the browser, use window.addEventListener('unhandledrejection', callback). In Node.js, use process.on('unhandledRejection', callback). Inside the callback, log the reason and the promise.
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection at:', event.promise, 'reason:', event.reason);
// Optionally, prevent the default console output
event.preventDefault();
});
Even if the reason has no stack, the event.promise object can be examined. You can set a conditional breakpoint in DevTools right here. But remember: this only fires after the rejection has occurred. You still need to find the source.
Wrapping Promise Bodies With Error Objects
The most reliable way to get a stack trace is to ensure every rejection is an Error instance. Compare these two snippets:
// Bad: stack trace is lost
const p = Promise.reject('Failed');
// Good: stack trace preserved
const p = Promise.reject(new Error('Failed'));
The same applies to new Promise((resolve, reject) => { ... }). Always call reject(new Error(...)). In async functions, always throw new Error(...). This seems trivial, but it is the root cause of countless missing traces.
Leveraging Async/Await for Better Stack Traces
When you use async/await, the JavaScript engine includes the async function in the stack trace. Even if the error is thrown deep inside a promise chain, the await point appears as a call site. This makes debugging much easier.
async function loadUser(id) {
const response = await fetch(`/api/user/${id}`);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
}
If the fetch fails, the stack trace will show loadUser and the line with await fetch. If you had used a raw promise chain with .then(), you might only see a generic <anonymous> line.
Production vs Development: Source Maps Matter
In production, your code is minified. The stack trace points to main.bundle.js:1:1234 which is useless. To interpret the trace, you need source maps uploaded to your error tracking service. Services like Sentry, Rollbar, and Datadog automatically map minified stack traces back to original lines, but only if you send the source maps during deployment.
Without source maps, the stack trace lines mean nothing. Always serve or upload source maps in a way that is accessible to your error reporter but not to the public (unless you set access controls).
Expert Advice
“I once spent a whole afternoon trying to figure out why our API client was swallowing stack traces. Turns out a team member had written a helper that called
reject('Network error')instead ofreject(new Error('Network error')). We added a lint rule to enforcenew Error()in all rejections and the problem vanished. The stack trace is your best clue, protect it.” – A senior engineer reflecting on a production outage.
Common Mistakes That Kill Stack Traces
Here are the traps that lead to an uncaught promise error no stack trace.
- Using
reject('message')instead ofreject(new Error('message')). This is the number one cause. - Catching an error and logging it, but not re-throwing. If you log and then forget to throw, the promise becomes resolved, and the error is lost.
- Returning
reject(...)inside a.then()callback. The return value of a.then()becomes the resolved value of the next promise. If you returnreject(), it resolves toundefinedand does not propagate the rejection. Always usethrowor return a rejected promise. - Not handling promise rejections in event listeners. Event listener callbacks that return a promise without a
.catch()will produce unhandled rejections when the page navigates away or the element is removed. - Minifying code without source maps. The stack trace exists, but you cannot read it.
What to Do When the Stack Trace Is Already Gone
Sometimes the error is already in your logs with no stack. What now?
- Replicate the exact scenario in local development with source maps enabled. If you can reproduce the error, the stack trace will appear in your DevTools because source maps are active. Use the same API endpoints, same user state.
- Add extra logging before every
reject()call. Temporarily placeconsole.trace()right before eachreject()in the suspect module. This prints the current call stack to the console at runtime. - Insert a
debugger;statement before the known rejection point. When the code hits that line, the debugger pauses and you can inspect the call stack. - Check the network tab. Many uncaught promise errors come from failed fetch or XMLHttpRequest calls. The network tab shows the request URL, status, and response. This gives you context even without a stack trace.
- Review your error logging setup. It is possible that your error tracking service is stripping the stack trace. Services like Sentry have options to “normalize” errors that can drop the stack if the error is not an
Errorinstance. Check the raw payload. For more on misleading error logging, see our guide on 5 signs your error logging setup is misleading you.
Getting to the Root of That Ghost Promise Error
An uncaught promise error no stack trace is not a dead end. It is a sign that somewhere in your code an error object was born without its lineage. The fix is almost always a combination of discipline (always use new Error()) and tooling (async stack traces, source maps, global handlers). Start by adding the global listener and enabling async traces in DevTools. Then run your app and reproduce the error. The trace may not be perfect, but you will at least know which module triggered it.
Next time you see that cryptic message, do not panic. Work through the numbered process above, and you will uncover the source. Consistent error handling practices will prevent the problem from reappearing. For a broader look at debugging asynchronous JavaScript, check out our guide on how to debug asynchronous JavaScript errors without losing your sanity. And if you suspect your production environment is hiding errors due to minification, learn about why your production errors don’t show up in local development.
The stack trace is your friend. Treat it with care, and it will never abandon you.
