Stack traces & the stack trace API
Learn when error.stack is captured, how V8's stack trace API customizes frames, how source maps rewrite locations, and what TC39 is standardizing.
- 01Separate capture from formattingExplain why V8 captures stack frames when an Error is constructed, then formats the stack lazily on first access.
- 02Use V8 stack APIs carefullyControl frame count with Error.stackTraceLimit, hide helper frames with Error.captureStackTrace, and inspect CallSite objects with Error.prepareStackTrace.
- 03Read mapped and future stacks honestlyDescribe how source maps change displayed file locations, and how the Error stack accessor proposal differs from older stack APIs.
A stack trace is a captured route
A stack trace is a list of active calls that led to a point in your program. When an error says at renderInvoice above at checkout, it is showing the path through function calls, newest frame first.
The stack trace API is the collection of mostly non-standard hooks around error.stack. In V8, an Error captures stack frames when it is constructed, formats them lazily when stack is first read, and lets controlled environments customize the frame count and formatter.
This lesson is not the everyday “how do I read an error?” path. For that, use Reading errors, Custom errors, try/catch, and the debugging lessons. Here we look at how the stack is built and where tool authors can hook into it.
- Create an error object near the interesting moment.
- The engine records frames, with engine-specific limits.
- Later code reads
error.stack, so the engine formats those frames. - Tooling may remap generated locations back to original source files.
We will stay synchronous in this lesson. The next lesson, Async stack traces, explains how await and promises add missing asynchronous history.
Captured when the Error is made
In V8, new Error() captures the stack when the object is constructed, not when it is thrown. That detail surprises people who create one error early, keep it around, and throw it somewhere else later.
A plate falls from a stack, so you take a photo immediately. When you show that photo later, it still shows the stack as it looked then. An Error stack works the same way: it points to where the Error was made, not where you later throw it.
- In real life: A plate falls from the stack
- In JavaScript:
new Error()is constructed - In real life: You take a photo right away
- In JavaScript: V8 captures the current stack frames
- In real life: You show the photo later
- In JavaScript: The same error object is thrown later
- In real life: The photo shows the stack at that moment
- In JavaScript: The stack still points at construction
Where the analogy stops: A photo cannot explain why a plate fell. An error stack is also limited diagnostic evidence, so create a fresh Error for a fresh stack.
Follow the moment a stack is captured. The Error is made on line 2, then thrown on line 8 without changing its original stack.
script
function makeError() { const error = new Error("plate fell"); return error;} function throwLater(error) { try { throw error; } catch (caught) { return caught.stack.split("\n").slice(0, 4); }} const firstLines = throwLater(capturedError);console.log(firstLines[1].includes("makeError"));console.log(firstLines.some((line) => line.includes("throwLater")));Line 2 constructs the error. Line 8 throws the same object later. The first printed value is true because the captured stack includes makeError. The second printed value is false because throwLater was not part of the original capture.
V8 keeps stack information internally, then formats the string on demand the first time error.stack is read. That is why custom formatters can still change what the first read returns.
This also explains a practical habit: create the error at the failure boundary. If you create it long before the failure, your logs will point at the factory, not the place where the decision to fail happened.
captureStackTrace and stackTraceLimit
Error.captureStackTrace(target, hiddenFunction) installs stack information on any object. The second argument asks V8 and supporting engines to omit frames above that function, including the function itself. Error helpers use this to hide plumbing.
Error.captureStackTraceJavaScriptfunction publicApi() { const marker = { name: "StackMarker" }; Error.captureStackTrace(marker, publicApi); return marker.stack.split("\n").slice(0, 4).map((line) => line.trim());} function userCode() { return publicApi();} const lines = userCode();console.log(typeof Error.captureStackTrace);console.log(lines[0]);console.log(lines[1].includes("userCode"));console.log(lines.some((line) => line.includes("publicApi")));Line 3 captures a stack on marker and hides publicApi. Line 14 proves the first useful frame is userCode. Line 15 proves the helper frame is gone. MDN's browser-compat data now lists modern Firefox and Safari support, but old browsers and smaller engines still need a feature check.
Take a photo of a stack of plates, but keep only the top ten in the picture. You can keep fewer, more, or every visible plate. The stack did not change; only the amount you kept changed.
- In real life: A stack holds plates in order
- In JavaScript: A stack records call frames in order
- In real life: Keep a photo of only the top ten plates
- In JavaScript:
Error.stackTraceLimitlimits collected frames - In real life: Leave a helper plate out of the photo
- In JavaScript:
captureStackTrace(obj, fn)omits helper frames - In real life: Keep every visible plate
- In JavaScript:
Infinityasks V8 to collect all available frames
Where the analogy stops: Real plates do not have a fixed number at the top. JavaScript stacks depend on the engine, host formatting, and privacy choices.
const previousLimit = Error.stackTraceLimit;Error.stackTraceLimit = 3; function deep(level) { if (level === 0) return new Error("limit check").stack; return deep(level - 1);} const lines = deep(6).split("\n");console.log(lines.slice(1).length);Error.stackTraceLimit = previousLimit;3Press Run to capture stack lines in this browser.
No stack captured yet. Choose a limit, then press Run.
Press Run to capture a stack in this browser. The lesson waits for your click before reading browser-specific stack text.
stackTraceLimit show a safe fallback. The source panel is a teaching model and is not an engine inspector.Error.stackTraceLimit defaults to 10 in V8. Setting it to 0 disables collection, a finite integer caps frames, and Infinity asks for all available frames. The setting is per V8 context, which usually means one page or iframe in Chrome.
| API | What it controls | Practical caution |
|---|---|---|
new Error() | Captures stack information in V8 when the error object is constructed. | Use it near the failing condition, not minutes before, if you want a useful creation site. |
error.stack | A de facto, non-standard property today; formats are different across engines. | Good for logs, not for portable parsing unless you control the engine. |
Error.captureStackTrace(obj, fn) | Installs a stack on any object and can hide frames through fn. | Great for error helpers, but guard it when supporting older browsers. |
Error.stackTraceLimit | V8-specific limit for how many frames are collected in the current context. | Raise temporarily for diagnostics; restore it so one page does not surprise another. |
Error.prepareStackTrace | V8-specific hook that receives an error and an array of CallSite objects. | Powerful for tools, risky as a global setting in apps. |
prepareStackTrace and CallSite objects
V8's Error.prepareStackTrace is a global formatter hook. When V8 formats a stack, it calls your function with the error and an array of CallSite objects. A CallSite is a structured object for one frame: it can report the function name, file name, line, column, whether the call was a constructor, and more.
Watch V8 hand structured CallSite objects to a custom formatter. This is powerful, global, and V8-specific.
script
Error.prepareStackTrace = (error, callSites) => { const first = callSites[0]; return { message: error.message, functionName: first.getFunctionName(), line: first.getLineNumber(), native: first.isNative(), };}; function inspectStack() { const error = new Error("custom format"); return error.stack;} const stack = inspectStack();console.log(stack.functionName);console.log(typeof stack.line);console.log(stack.native); Error.prepareStackTrace = previous;The example returns an object instead of a string to prove that V8 accepts any formatter result. Line 7 reads the newest CallSite's function name. Line 8 stores its line number. Line 23 restores the previous formatter, which is not optional in real tools because the hook affects every stack formatted in that context.
V8 documents methods such as getFunctionName(), getFileName(), getLineNumber(), getColumnNumber(), isNative(), isConstructor(), and getTypeName(). They are useful in Node diagnostics and test tools, but they are not portable language features.
If you customize this hook, keep the formatter tiny, wrap changes in try/finally, and avoid shipping a global formatter in a browser app unless you own the whole runtime. Logging frameworks and test runners are better homes for it.
Source-mapped stack traces
Production JavaScript often runs after bundling, minifying, or compiling TypeScript. A source map is a file that maps a generated location back to the original source location. DevTools uses maps for browser stacks, and Node can do the same with --enable-source-maps.
// compiled-stack-demo.js, produced by a build toolfunction crash() { throw new Error("mapped");}crash();//# sourceMappingURL=data:application/json;base64,...The generated file still executes. The map only changes reported locations. The lesson test runs a child Node process with an inline source map and --enable-source-maps, then asserts that the thrown stack names original-stack-demo.ts. That is a tooling fact, not a new JavaScript semantic rule.
The test builds a tiny data-URL source map in memory, runs Node as a child process, and checks stderr for the original file name. It does not assert exact columns or absolute paths because those differ by platform.
In a real site, publish source maps only with the privacy level your team accepts. They are excellent for debugging, but they can reveal original names and comments if you upload full maps publicly.
Proposals and engine support
Today, error.stack is a de facto property, not fully standardized behavior. The TC39 Error stack accessor proposal is Stage 3 and focuses on standardizing the long-existing Error.prototype.stack accessor shape. The older broader Error Stacks proposal remains Stage 1.
const descriptor = Object.getOwnPropertyDescriptor(new Error("proposal"), "stack");console.log(typeof descriptor.get);console.log(typeof descriptor.set);console.log(descriptor.enumerable);console.log(descriptor.configurable);Node 22 with V8 12.4 prints function, function, false, and true for that descriptor. Safari's historical shape differs, and Firefox has used a prototype accessor. That is exactly why TC39 is standardizing the common surface carefully instead of standardizing every engine's stack internals.
| Feature | Current support shape | How to use it |
|---|---|---|
error.stack | Non-standard today, but browsers and Node expose it with different formats. | TC39 is standardizing an accessor, not a universal string format. |
Error.captureStackTrace | Chrome has long supported it; MDN BCD lists Firefox 138+, Safari 17.2+, Node 0.10+, Deno, and Bun too. | Still check before calling when your code can run in older or embedded engines. |
Error.stackTraceLimit | Documented by V8 and Node/V8; not a JavaScript language feature. | Use only as an engine-specific diagnostic knob. |
Error.prepareStackTrace and CallSite | Documented by V8. Other engines may ignore it or expose different internals. | Use for Node tooling and controlled Chrome/V8 environments, not portable app logic. |
`throw new Error("boom")`- The Error stack accessor proposal
`error.stack``Error.captureStackTrace``Error.stackTraceLimit``Error.prepareStackTrace``CallSite#getLineNumber()``node --enable-source-maps app.js`
Decide whether each card is standard/proposal work, a de facto cross-engine behavior, V8-specific stack API, or host tooling.
Use stack power safely
Working developers usually need stacks for three jobs: logs, test diagnostics, and developer tools. In app code, prefer simple custom error classes and clear messages. Reach for V8 hooks only when you control the runtime or are writing tooling for Node and Chrome.
- Create fresh errors at the failure boundary so the construction stack is meaningful.
- Use
captureStackTraceto hide helper frames, with a feature check for older engines. - Raise
stackTraceLimitonly around diagnostics, then restore it. - Restore
prepareStackTraceinfinally; it is global in the current context. - Enable source maps in development and production observability only when the map exposure is acceptable.
Browser debugging lessons such as Debugging in the browser and Advanced debugging cover DevTools workflows. This lesson gives you the engine-level vocabulary for understanding what those tools display.
Common misconceptions
- “The stack is captured when I throw.” In V8, normal Error stacks are captured when the Error is constructed.
- “`error.stack` is standardized like `message`.” The property exists widely, but exact behavior and formatting are still being standardized.
- “Source maps change what ran.” They change reported source locations, not runtime execution.
- “`prepareStackTrace` is a safe app-level formatter.” It is a V8-specific global hook. Use it in controlled tooling and restore it.
- “A longer stack is always better.” Longer stacks cost memory and can expose private implementation details.
| Pair | Correct idea | Do not confuse it with |
|---|---|---|
| Captured vs thrown | V8 captures frames when the Error is created. | Throwing the same object later does not rewrite its creation stack. |
| Captured vs formatted | Captured frames are kept internally; the string is made on first stack access. | Formatting is not the same moment as capture. |
| Source map vs runtime | A source map remaps displayed file, line, and column information. | It does not change which JavaScript actually executed. |
| Proposal vs V8 API | The Stage 3 accessor proposal standardizes Error.prototype.stack shape. | It does not standardize prepareStackTrace, stackTraceLimit, or CallSite methods. |
Practice exercises
Read the snippet and type the two printed boolean values.
function makeError() {
return new Error("packed");
}
function deliver(error) {
try {
throw error;
} catch (caught) {
return caught.stack;
}
}
const error = makeError();
const stack = deliver(error);
console.log(stack.includes("makeError"));
console.log(stack.includes("deliver"));It prints true and then false. The stack contains makeError because construction captured it, but it does not contain deliver for this reused error.
Which function name should remain as the first useful stack frame?
function publicApi() {
const marker = { name: "StackMarker" };
Error.captureStackTrace(marker, publicApi);
return marker.stack.split("\n")[1];
}
function userCode() {
return publicApi();
}
console.log(userCode().includes("userCode"));The first useful frame is userCode. Passing publicApi removes the helper itself and frames above it from the stack.
With a limit of 2, how many stack frame lines are collected?
const oldLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 2;
function a() {
return b();
}
function b() {
return new Error("short").stack.split("\n").slice(1).length;
}
console.log(a());
Error.stackTraceLimit = oldLimit;It prints 2. Error.stackTraceLimit = 2 keeps two at ... frame lines after the message line.
The starter code is safe because it restores a global hook. Name the thing that must be restored.
const previous = Error.prepareStackTrace;
try {
Error.prepareStackTrace = () => "short";
console.log(new Error("x").stack);
} finally {
Error.prepareStackTrace = previous;
}Restore Error.prepareStackTrace to the saved previous formatter in finally. Otherwise later stacks in the same context keep the custom formatter.
In the lesson proof, which original file name should Node show after applying the inline source map?
A mapped stack should show original-stack-demo.ts. Your production tooling may upload maps to an error service instead of exposing them publicly.
What TC39 stage did the verified Error stack accessor proposal README report?
The verified README says the Error stack accessor proposal is Stage 3. Stage 3 is advanced, but still not the same as finished ECMAScript.
Quiz
Check the difference between construction, throwing, formatting, mapping, and standardization. The code questions are proved by the lesson tests in Node/V8.
Question 1 of 7In V8, when is the stack trace for a normal
Errorcaptured?Choose an answer to see the explanation.
Question 2 of 7What does this reused-error snippet print?
Read the code, then predictconst error = new Error("ready"); function throwLater() { try { throw error; } catch (caught) { console.log(caught.stack.includes("throwLater")); console.log(caught.stack.includes("ready")); } } throwLater();Choose an answer to see the explanation.
Question 3 of 7Which support statement is accurate for
Error.captureStackTracetoday?Choose an answer to see the explanation.
Question 4 of 7What does the stack limit snippet print in V8/Node?
Read the code, then predictJavaScriptconst oldLimit = Error.stackTraceLimit; Error.stackTraceLimit = 1; function a() { return b(); } function b() { return new Error("short").stack.split("\n").slice(1).length; } console.log(a()); Error.stackTraceLimit = oldLimit;Choose an answer to see the explanation.
Question 5 of 7What does
prepareStackTracereturn here?Read the code, then predictJavaScriptconst oldPrepare = Error.prepareStackTrace; Error.prepareStackTrace = (error, sites) => sites[0].getFunctionName(); function sample() { return new Error("x").stack; } console.log(sample()); Error.prepareStackTrace = oldPrepare;Choose an answer to see the explanation.
Question 6 of 7What does a source map change in a stack trace?
Choose an answer to see the explanation.
Question 7 of 7Which proposal status did the verified TC39 READMEs report?
Choose an answer to see the explanation.
Key takeaways
- In V8, normal error stacks are captured at construction time and formatted lazily on first access.
Error.captureStackTracecan install a stack and hide helper frames;Error.stackTraceLimitcontrols V8 frame count.Error.prepareStackTracereceives V8 CallSite objects, but it is global and engine-specific.- Source maps remap displayed locations; they do not change runtime call order.
- The Error stack accessor proposal is Stage 3; it standardizes the stack property surface, not V8's formatter API.
Treat stacks as captured diagnostic evidence: create errors at the right moment, customize only in controlled runtimes, and map generated locations back to source when tooling can do it safely.
Next, Async stack traces shows how engines reconstruct stack history across await, promise combinators, and callback boundaries.