How to Track Down the Elusive ‘Uncaught Promise’ Error That Has No Stack Trace

How to Track Down the Elusive 'Uncaught Promise' Error That Has No Stack Trace

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.

Key Takeaway

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 undefined to reject() instead of an Error instance.
  • Throwing a non-Error value (e.g., throw 42) inside an async function.
  • 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 using Error.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.

  1. Add a global unhandledrejection event listener. This catches every unhandled promise rejection in the window. Inside the handler, log the promise and the reason. In modern browsers, the promise property 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.

  2. 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 new Error object that includes the original reason. This preserves the trace.

  3. Wrap every promise rejection in new Error() or a custom error constructor. Never pass raw strings. Always do reject(new Error('descriptive message')). For async functions, always throw an Error object. This is the single most effective habit.

  4. Use async/await with try/catch blocks. Inside an async function, the stack trace includes the function name and the location of the await that caused the error. This works because the engine creates a new async context that chains the stack.

  5. Set a breakpoint in the unhandledrejection handler 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 of reject(new Error('Network error')). We added a lint rule to enforce new 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 of reject(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 return reject(), it resolves to undefined and does not propagate the rejection. Always use throw or 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?

  1. 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.
  2. Add extra logging before every reject() call. Temporarily place console.trace() right before each reject() in the suspect module. This prints the current call stack to the console at runtime.
  3. 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.
  4. 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.
  5. 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 Error instance. 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.

By theo

Leave a Reply

Your email address will not be published. Required fields are marked *