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.
- 01Know when a stack is madeExplain why V8 writes down the list of calls when an error is made, and turns it into text only when
stackis first read. - 02Use V8's stack tools with careSet how many calls are kept with
Error.stackTraceLimit, hide helper functions withError.captureStackTrace, and read CallSite objects withError.prepareStackTrace. - 03Read mapped stacks and know what is officialDescribe how source maps change the file names a stack shows, and how the Error stack accessor proposal differs from V8's own tools.
A stack trace is a list of calls
A stack trace is a list of the function calls that were still running at one moment in your program. Each line in the list is one call. Tools call one line a frame. The newest call is at the top.
Say an error shows at renderInvoice above at checkout. That means checkout called renderInvoice, and the error happened inside renderInvoice.
Every error object has a stack property. It holds the stack trace as text. The stack trace API is a small set of tools around error.stack. Most of them are not in the official JavaScript rules.
Many of these tools come from V8. V8 is the engine, the program that runs JavaScript, inside Chrome, Edge, and Node.js. In V8:
- An error writes down the list of calls when it is made.
- It turns that list into text only when
stackis first read. - You can change how many calls it keeps and how the text looks.
This lesson is not about reading an everyday error. For that, see Reading errors, Custom errors, try/catch, and the debugging lessons. Here you will see how the list is made, and where tools can change it.
A stack trace goes through four steps:
- Your code makes an error object.
- The engine writes down the calls that are running. Each engine has its own limit on how many it keeps.
- Later, some code reads
error.stack. Only then does the engine turn the list into text. - A tool may change the file names and line numbers in that text to point at the files you wrote.
All the code in this lesson runs straight through, one line after another. Code that waits, with await or promises, comes in the next lesson, Async stack traces.
The list is written down when the error is made
In V8, new Error() writes down the list of calls at the moment the error object is made. It does not wait until the error is thrown. This surprises people who make an error early, keep it, and throw it later from somewhere else.
A plate falls from a stack of plates, so you take a photo right away. Later you show someone the photo. It still shows the plates as they were when you took it. An error's stack works the same way: it shows where the error was made, not where you throw it.
- In real life: A plate falls from the stack
- In JavaScript:
new Error()makes an error object - In real life: You take a photo right away
- In JavaScript: V8 writes down the calls that are running
- 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 as it was
- In JavaScript:
error.stackstill shows where the error was made
In the code below, top-level code means code that is not inside any function. Step through it and watch line 2 and line 8.
Watch when the list of calls is written down. The error is made on line 2 and thrown on line 8, and its stack still shows line 2.
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 makes the error inside makeError. Line 8 throws the same object later, inside throwLater.
- The first
console.logprintstrue. The stack namesmakeError, because the list was written down there. - The second prints
false.throwLaterwas not running when the list was written down, so it is not in it.
new Error() runs. Throwing the same error on line 8 does not change that list, so throwLater is not in it. (Real stack lines also show a file name and a line number.)V8 keeps the list of calls hidden inside the error. It turns the list into text only the first time some code reads error.stack. That is why your own function for the text, which you will meet below, can still change what that first read gives back.
This gives a useful habit: make the error at the place where things go wrong. If you make it much earlier, your logs will point at the function that made the error. They will not point at the place where your code decided to fail.
Hide a helper, and choose how many calls to keep
Error.captureStackTrace(target, hiddenFunction) writes a stack onto any object you give it, as a stack property. It takes two arguments:
target: the object that gets thestackproperty.hiddenFunction(optional): a function to leave out. The list then starts just below this function. The function itself and every call above it are left out.
Error helpers use this to hide their own code, so the stack starts at the code that called them.
function 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 by line:
- Line 2 makes a plain object,
marker. Itsnamebecomes the first line of its stack. - Line 3 writes a stack onto
markerand leaves outpublicApi. - Line 4 cuts the stack text into lines with
split("\n"), keeps the first four withslice(0, 4), and removes spaces at each end withtrim(). - Line 12 prints
function, so this engine hascaptureStackTrace. - Line 13 prints
StackMarker, the first line. - Line 14 prints
true: the first call in the list isuserCode. - Line 15 prints
false.some()asks "does any line namepublicApi?" None does.
publicApi, says "start the list below this function". So the list starts at userCode, the code that called the helper. (Real stack lines also show a file name and a line number.)captureStackTrace started in V8. MDN's browser support tables now list it in new Firefox and Safari too. Old browsers and small engines may not have it, so check that it exists before you call it.
You take a photo of a stack of plates, but you keep only the top ten in the picture. You could keep fewer, more, or every plate. The stack of plates is the same. Only the number you keep in the photo changes.
- In real life: A stack holds plates in order
- In JavaScript: A stack trace lists calls in order
- In real life: Keep only the top ten plates in the photo
- In JavaScript:
Error.stackTraceLimitsets how many calls are kept - In real life: Leave one plate out of the photo
- In JavaScript:
captureStackTrace(obj, fn)leaves helper calls out - In real life: Keep every plate in the photo
- In JavaScript:
Infinityasks V8 to keep every call
In the playground, deep(6) calls itself again and again until its number reaches 0: seven deep calls in a row. The last one makes the error. Pick a limit, press Run, and count the calls that are kept.
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 get the stack lines from this browser.
No stack yet. Pick a limit, then press Run.
Press Run to make an error in this browser. Nothing runs until you press it.
stackTraceLimit, you see what it gives back without a limit.Error.stackTraceLimit is a number that V8 reads each time an error is made. It is 10 unless you change it.
0keeps no calls at all.- A whole number, like
3, keeps at most that many calls. Infinitykeeps every call.
The setting belongs to one page, or one iframe, in Chrome. V8 calls this a context. Changing it on one page does not change it on another.
Each row of this table is one stack tool. Read across to see what it does and what to be careful about.
| What you write | What it does | What to be careful about |
|---|---|---|
new Error() | In V8, writes down the list of calls at the moment the error is made. | Make it where things go wrong, not long before, so the stack points at the right place. |
error.stack | Holds the list as text. It is not in the official rules yet, and each engine writes it a little differently. | Good for logs. Don't pick it apart in code unless you know which engine runs it. |
Error.captureStackTrace(obj, fn) | Writes a stack onto any object, and can leave out fn and the calls above it. | Great for error helpers. Check that it exists first if old browsers must work. |
Error.stackTraceLimit | A V8 number: how many calls an error keeps, on this page. | Raise it only while you look for a bug, then put it back. |
Error.prepareStackTrace | A V8 setting: your function gets the error and a list of CallSite objects, and returns the stack. | Useful in tools. Risky in a website, because it changes every stack on the page. |
Make your own stack text with prepareStackTrace
Error.prepareStackTrace is a V8 setting that holds a function. V8 calls that function when it turns a list of calls into stack text. It passes in two things:
- the error object;
- an array of CallSite objects, newest call first. A CallSite describes one call in the list.
A CallSite has methods that answer questions about its call: the function name, the file name, the line, the column, whether the function was called with new, and more. Whatever your function returns becomes the value of error.stack.
Watch V8 pass CallSite objects to your own function. This setting changes every stack on the page, and only V8 has it.
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;This example returns an object instead of text. That shows V8 accepts any value your function returns.
- Line 1 saves the old function in
previous. - Line 3 puts in the new function.
- Line 7 asks the newest CallSite for its function name. It is
inspectStack, the function that made the error. - Line 8 asks for its line number.
- Line 23 puts the old function back. Real tools must do this. The setting changes every stack on the page, not just this one.
error.stack is read, V8 calls your function with the error and the list of calls. Whatever your function returns becomes the stack. Here it is an object whose functionName is inspectStack.V8's documentation lists methods such as getFunctionName(), getFileName(), getLineNumber(), getColumnNumber(), isNative(), isConstructor(), and getTypeName(). They are useful in Node.js tools and test tools. They are not part of the JavaScript language, so other engines may not have them.
If you change this setting:
- keep your function very small;
- put the old function back inside
finally, the block that runs whether or not an error is thrown; - don't change it in a website unless you control all the code on the page.
Logging tools and test runners are better places for it.
Source maps point the stack at the file you wrote
The JavaScript that runs on a real website is often not the code you wrote. A build tool changes it first. It may:
- join many files into one (this is called bundling);
- shorten names and remove spaces to make the file smaller (minifying);
- turn TypeScript into JavaScript.
Then the stack trace names the built file, not yours. A source map is a file that links each place in the built file back to a place in the file you wrote. Browser DevTools use source maps for stack traces. Node.js does too, when you start it with --enable-source-maps.
// compiled-stack-demo.js, produced by a build toolfunction crash() { throw new Error("mapped");}crash();//# sourceMappingURL=data:application/json;base64,...Line 3 throws the error. Line 6 is a comment that tells tools where the source map is. Here the map is written right into the comment (the ... stands for the long map text).
original-stack-demo.ts. (Line and column numbers are left out here.)The built file is still the code that runs. The map only changes the file names and line numbers that the stack trace shows. It is a feature of the tools, not a rule of the JavaScript language.
The lesson test writes a tiny source map right into the code, as text. It starts Node.js with --enable-source-maps, runs the code, and checks that the error output names original-stack-demo.ts. It does not check exact column numbers or full folder paths, because those are different on each computer.
Be careful when you put source maps on a public website. They are great for finding bugs. But anyone can download them, and a full map can show your original code, names, and comments. Your team should decide how much it is happy to share.
Which stack tools work everywhere?
TC39 is the group that writes the official rules of JavaScript. A new idea goes through stages, from Stage 0 to Stage 4. Stage 4 means it is finished and part of the language. Stage 3 means it is nearly finished.
Today, error.stack is not fully in the official rules. Every major engine has it, but each one behaves a little differently. There are two TC39 proposals about it:
- The Error stack accessor proposal is at Stage 3. It writes down the shape that
Error.prototype.stackhas had for years. - The older, bigger Error Stacks proposal is still at Stage 1.
An accessor is a property that runs a function when you read it (a getter) or write it (a setter). Object.getOwnPropertyDescriptor returns a small object that describes one property. For an accessor, its get and set fields hold those two functions.
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);In Node.js 22 (with V8 12.4), this prints:
- Line 2:
function. There is a getter. - Line 3:
function. There is a setter. - Line 4:
false.stackdoes not show up when you loop over the error's properties. - Line 5:
true. The property can be changed or deleted.
Other engines are different. Safari has used a different shape in the past. Firefox has kept the getter on Error.prototype, not on each error. That is why TC39 is writing down only the shared part. It is not trying to make every engine's stack text the same.
Each row of this table is one stack tool. Read across to see where it works today and how to use it.
| What you write | Where it works today | How to use it |
|---|---|---|
error.stack | Not in the official rules yet. Browsers and Node.js all have it, but their text looks different. | TC39 is writing down the getter and setter, not one text format for everyone. |
Error.captureStackTrace | Chrome has had it for a long time. MDN's support tables also list Firefox 138+, Safari 17.2+, Node.js 0.10+, Deno, and Bun. | Check that it exists before you call it, if your code can run in old or small engines. |
Error.stackTraceLimit | Described in the V8 and Node.js docs. It is not part of the JavaScript language. | Use it only as a V8 setting while you look for a bug. |
Error.prepareStackTrace and CallSite | Described in the V8 docs. Other engines may ignore it or work differently. | Use it in Node.js tools, or in Chrome when you control the page. Don't rely on it in a website. |
`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`
Put each card in a group: official JavaScript or a TC39 proposal; not official but in every major engine; V8 only; or a feature of a tool such as Node.js.
Use stack tools safely
Developers mostly need stack traces for three jobs:
- logs, which record what went wrong on a live site;
- test results, which show where a failing test broke;
- developer tools, such as debuggers and error reporters.
In everyday website code, use simple custom error classes and clear messages. Use the V8 settings only when you control everything that runs, or when you are writing a tool for Node.js or Chrome.
- Make a new error at the place where things go wrong, so its stack points there.
- Use
captureStackTraceto hide helper functions. Check that it exists first, for older engines. - Raise
stackTraceLimitonly while you look for a bug. Then put the old value back. - Put the old
prepareStackTraceback insidefinally. It changes every stack on the page. - Turn on source maps for your live site only if your team is happy for people to see the original code.
The lessons Debugging in the browser and Advanced debugging show how to use DevTools. This lesson explains what is behind the stack traces those tools show.
Common misconceptions
- "The stack is written down when I throw." In V8, an error's stack is written down when the error is made.
- "
error.stackis in the official rules, likemessage." Every major engine has it, but the exact behavior and the text are still being written into the rules. - "Source maps change what runs." They only change the file names and line numbers that a stack trace shows.
- "
prepareStackTraceis safe to change in any website." It is a V8 setting that changes every stack on the page. Use it only in tools you control, and put the old one back. - "A longer stack is always better." Longer stacks use more memory. They can also show private details about how your code works.
Each row of this table is one pair of ideas that people mix up. Read across: the right idea, then what it is not.
| Which two ideas? | What is true | What it does not mean |
|---|---|---|
| Made or thrown | V8 writes down the list of calls when the error is made. | Throwing the same error later does not write a new list. |
| Written down or turned into text | V8 keeps the list hidden, and makes the text the first time stack is read. | Making the text is a separate, later step. |
| Source map or what runs | A source map changes the file, line, and column a stack shows. | It does not change which JavaScript runs. |
| The proposal or V8's tools | The Stage 3 proposal writes down how Error.prototype.stack works. | It does not add prepareStackTrace, stackTraceLimit, or CallSite methods to the rules. |
Practice exercises
Read the code and type the two values it prints.
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, then false. The stack names makeError, because the list of calls was written down when the error was made there. It does not name deliver, because throwing the same error later does not add anything.
Which function name is the first call left in the stack?
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 call in the list is userCode. Passing publicApi leaves out the helper itself and every call above it.
With a limit of 2, how many call lines does the stack keep?
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 ... lines after the message line.
This code is safe because it puts a page-wide setting back at the end. Name the setting it puts back.
const previous = Error.prepareStackTrace;
try {
Error.prepareStackTrace = () => "short";
console.log(new Error("x").stack);
} finally {
Error.prepareStackTrace = previous;
}Put Error.prepareStackTrace back to the saved previous function inside finally. If you don't, every later stack on the page keeps your custom text.
In the lesson test, which file name does Node.js show once it uses the source map?
With the source map, the stack shows original-stack-demo.ts. On a real site, your tools may send the maps to an error-reporting service instead of putting them on the public website.
What TC39 stage is the Error stack accessor proposal at, according to its page?
The proposal's page says the Error stack accessor proposal is at Stage 3. Stage 3 is nearly finished, but it is not yet part of the official language.
Quiz
These questions check five moments: making an error, throwing it, turning the stack into text, mapping it to your files, and what is in the official rules. The lesson tests run each code question in Node.js to check the answer.
Question 1 of 7In V8, when does an error write down its list of calls?
Choose an answer to see the explanation.
Question 2 of 7This error is made first and thrown later. What does the code 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 sentence about
Error.captureStackTraceis true today?Choose an answer to see the explanation.
Question 4 of 7With a limit of 1, what does this code print in Node.js?
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 this custom
prepareStackTracemake the stack?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 7What stage are the two TC39 stack proposals at?
Choose an answer to see the explanation.
Key takeaways
- In V8, an error writes down its list of calls when it is made. It turns the list into text only when
stackis first read. Error.captureStackTracewrites a stack onto any object and can leave helper functions out.Error.stackTraceLimitsets how many calls V8 keeps.Error.prepareStackTracegets a list of V8 CallSite objects. It changes every stack on the page, and only V8 has it.- Source maps change the file names and line numbers a stack shows. They don't change what runs.
- The Error stack accessor proposal is at Stage 3. It covers the
stackproperty, not V8's own settings likeprepareStackTrace.
A stack is a record of which calls were running: make errors at the right moment, change stack settings only in code you control, and use source maps to point stacks back at the files you wrote.
Next, Async stack traces shows how engines rebuild the list of calls when code waits, with await, promises, and callbacks.