You will learn how to trace failures in JavaScript asynchronous code by using async stack traces, breakpoints, and error logging to see where a promise or await actually failed.
What it is
Debugging async code means finding the cause of a problem that happens after the current synchronous call stack has finished. JavaScript schedules callbacks, promise continuations, and await resumptions through the event loop, so a normal stack trace may stop at setTimeout, Promise, or an internal scheduler instead of showing the function that started the work.
The useful mental model is two timelines: the synchronous call stack and the asynchronous continuation queue. Related terms include event loop, microtasks, promise rejection, async stack traces, and source maps.
Why it matters
- It reveals which async function called the failing step.
- It helps locate unhandled promise rejections.
- It makes race conditions and timing bugs easier to inspect.
- It preserves useful error context when promises are chained.
- It reduces guesswork when logs only show a generic error message.
Syntax or steps
The smallest useful pattern is to log the full error stack and enable async stack traces when available.
- Enable async stack traces in DevTools so the debugger can reconstruct logical async call chains.
- Put a breakpoint on the line containing
await, or on the line after it, to inspect values before and after the promise settles. - Wrap risky async calls in
try/catch, or attach.catch()to a promise chain. - Log
err.stack, not onlyerr.message, so the trace is preserved.
Example
function fetchProfile(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id < 0) {
reject(new Error("profile id must be non-negative"));
} else {
resolve({ id, name: "Ada" });
}
}, 10);
});
}
async function loadDashboard(id) {
const profile = await fetchProfile(id);
return { title: "Welcome", user: profile.name };
}
async function main() {
try {
const dashboard = await loadDashboard(-1);
console.log(dashboard);
} catch (err) {
console.error("main failed:", err.stack);
}
}
main();
fetchProfile returns a promise that settles later inside setTimeout. If id is negative, it rejects with an Error. loadDashboard uses await, so its execution pauses until the promise settles. main catches the rejection and logs err.stack. With async stack traces enabled, the stack can show the logical path through main and loadDashboard, even though the actual rejection happened in a timer callback.
Common mistakes
- Logging only
err.message. Fix: logerr.stackor pass the error object to a logger that preserves stack traces. - Forgetting to
awaita promise. Fix: useawaitor attach.catch()so rejections are handled. - Setting a breakpoint only on the synchronous call. Fix: set it on the
awaitline or on the rejection path. - Swallowing errors in a
.then()chain. Fix: return the promise and add a final.catch()that logs the error.
When to use it
Use async stack traces when the failure is deep in a promise chain and the normal stack is truncated. Use breakpoints when you need to inspect variables at a known async boundary. Use try/catch or .catch() when the program must recover or report the failure.
| Technique | Best for | Use when |
|---|---|---|
| Async stack traces | Seeing the full await/promise call chain | An error is thrown inside async code and the normal stack is incomplete |
| Breakpoints on await lines | Inspecting values before and after a promise settles | You know the suspicious async step but not the exact failure |
| try/catch or .catch | Handling or reporting failures | The program must continue after a rejected promise |
Practice
- Guided: Run the example, then change
loadDashboard(-1)toloadDashboard(1). Expected output: an object like{ title: "Welcome", user: "Ada" }. - Challenge: Add another async function that calls
loadDashboardand rejects with a different error. Hint: logerr.stackin the outermost catch and enable async stack traces.
Quick check
Question: Why might a normal stack trace not show the function that called an async callback?
Answer: Because the callback runs later from the event loop after the original synchronous stack has unwound; async stack traces reconstruct the logical call chain.
Summary
Async debugging works by connecting the visible error to the asynchronous continuation that caused it. Enable async stack traces, inspect await boundaries, and log full error stacks. This turns truncated traces into a practical map of the async flow.