TL;DR
A stack trace is the list of function calls that were in progress when an error happened, newest first. Each line, called a frame, names a function and the file, line and column it had reached. Read it from the top: the first frame is where the error was thrown, and every frame below it called the one above.
In this guide
What does a stack trace look like?
Here's a small checkout script. onCheckout calls loadCart, which fetches an order and hands it to formatTotal.
function formatTotal(order) { return order.totals.grand.toFixed(2);}async function loadCart(id) { const res = await fetch(`/api/orders/${id}`); const order = await res.json(); return formatTotal(order);}async function onCheckout() { await loadCart(42);}
The API answered with an order that had no totals. This is the stack trace Chrome printed, word for word apart from the address:
TypeError: Cannot read properties of undefined (reading 'grand') at formatTotal (https://shop.example.com/assets/cart.js:2:23) at loadCart (https://shop.example.com/assets/cart.js:8:10) at async onCheckout (https://shop.example.com/assets/cart.js:12:3)
The first line is the error itself: its type, then the message. Everything after it is the stack.
How to read a stack trace
Start at the top frame. formatTotal (https://shop.example.com/assets/cart.js:2:23) says the error was thrown inside formatTotal, in cart.js, on line 2, at column 23. Columns count characters from 1, and column 23 lands on grand, the property that couldn't be read.
Then read down. Line 8 of loadCart called formatTotal, and line 12 of onCheckout called loadCart. That's the stack: who called whom, at the moment it broke.
On a real site the top few frames are often someone else's code, like a framework, a date library or a polyfill. Skip down to the first frame in a file you wrote. That's usually the line to open first.
Usually, not always. The top frame is where the code broke, which isn't always where the mistake is. formatTotal did nothing wrong here. It was handed an order without totals, and the real question is why the API sent one, which no frame in this trace can answer. I'd read every stack trace as a map of where to start looking, never as the answer.
What are async frames in a stack trace?
An async frame is a line marked async that shows an await the error passed through on its way up. Chrome and Node print at async onCheckout, Firefox prefixes the function with async*, and Safari writes async onCheckout. Without them, the stack would stop at the first await.
They only follow await. A callback from setTimeout, an event listener or a .then() chain starts with a fresh stack, so the trace stops where that callback began, even though something else scheduled it. Chrome's console can show those scheduled-from links when you expand an error there, but the error.stack text your error logging receives doesn't have them.
Chrome and Node also keep only 10 frames by default. Deep framework call chains can lose the bottom of the stack, including the code that started it. Error.stackTraceLimit = 50 raises the limit for errors created after you set it, which is handy while debugging locally.
How stack traces differ between browsers
The same error from the same code reads differently in each engine. These are the top lines Firefox and Safari printed for the example above.
- Error message
Cannot read properties of undefined (reading 'grand')- Top frame
at formatTotal (…/cart.js:2:23)
- Error message
can't access property "grand", order.totals is undefined- Top frame
formatTotal@…/cart.js:2:3
- Error message
undefined is not an object (evaluating 'order.totals.grand')- Top frame
formatTotal@…/cart.js:2:22
The columns disagree because each engine points at a slightly different spot on the same line. Firefox and Safari write function@file:line:column with no "at" and no brackets. Notice that their messages name the whole expression that was undefined, which Chrome's doesn't.
Why production stack traces are unreadable
Production JavaScript is minified. The build squashes every file onto a few long lines and renames functions to single letters, so the same error from a minified build of the example looks like this:
TypeError: Cannot read properties of undefined (reading 'grand') at n (https://shop.example.com/assets/index-B7c2x9Qe.js:1:37) at a (https://shop.example.com/assets/index-B7c2x9Qe.js:1:133) at async r (https://shop.example.com/assets/index-B7c2x9Qe.js:1:157)
Function a at line 1, column 133 tells you nothing, and in a real bundle that line is hundreds of kilobytes long. A source map is the file that translates it back to formatTotal in cart.js on line 2. Browser developer tools apply it automatically when the map is published, but the error.stack text sent to an error tracker stays minified, so the tool has to apply the map itself. Rebase does that for the console errors in a report, reading up to 12 frames across 5 files, as long as your maps are reachable over HTTPS (how source context works).
For support teams
When a customer or teammate sends an error, ask for the whole stack trace as text, not a screenshot of the first line. The message says what broke. The frames say where, and engineering needs both. In the console, click the arrow next to the red error to expand it, then select and copy everything down to the last frame.
More on Console and dev tools