JAVASCRIPT / ERRORS, STRICT MODE, AND DEBUGGING
Reading stack traces to the failing line
Read a JavaScript stack trace top-down, skip built-in and library frames, and jump straight to the line and column in your own code that actually failed.
What you will learn
- Read frames top-down: the top frame is the newest call, the bottom is the outermost.
- Skip built-in and node_modules frames; open the topmost frame from your own files.
- Use a frame's line:column to land on the exact failing expression.
- Spot stacks cut short by an async boundary or by Error.stackTraceLimit (default 10).
Understanding Reading stack traces to the failing line
A stack trace is a list of the calls that had not returned yet at the moment the Error object was constructed. V8 prints a header line of the form `Name: message`, then one `at function (file:line:column)` line per frame, innermost call first, because the innermost call is the top of the stack being dumped. The capture happens inside `new Error(...)`, not at the `throw`, so an error object built in one place and thrown later still points at where it was built.
The topmost frame is where execution was, but not always where you can act: `at JSON.parse (<anonymous>)` or `at Array.map (<anonymous>)` means a built-in was running, and a path inside node_modules means a library was. Errors usually surface a call or two below the actual mistake, at the moment a bad value is finally dereferenced, so the useful starting point is the topmost frame whose path is a file you wrote. The column matters as much as the line: on `order.customer.address.city` it tells you which of the three property reads is the one that hit undefined.
Frames can be missing, and knowing why saves time. A callback run by a timer or an event listener starts on a fresh stack, so the function that called setTimeout or addEventListener is simply not in the trace; errors crossing an `await` do keep their caller frames, because V8 stitches the resumed stack back together. V8 also records only the innermost `Error.stackTraceLimit` frames, ten by default, which is why deep recursion arrives truncated. Line and column numbers describe the file the engine actually loaded, so bundled or transpiled code needs source maps before those numbers mean anything in your source.
Treat the trace as an answer to "how did control reach this line", not "who produced this bad value"; the second answer is found by walking down the frames and logging what each caller passed in.
function toCents(price) {
if (typeof price !== 'string') {
throw new TypeError('price must be a string, got ' + typeof price);
}
return Math.round(Number(price) * 100);
}
function lineTotal(item) {
return toCents(item.price) * item.qty;
}
function cartTotal(items) {
let cents = 0;
for (const item of items) {
cents += lineTotal(item);
}
return cents;
}
try {
cartTotal([{ price: '1.50', qty: 2 }, { price: 3, qty: 1 }]);
} catch (err) {
console.log(err.stack.split('\n')[0]);
err.stack.split('\n').slice(1, 4).forEach((frame, i) => {
console.log(i === 0 ? 'threw in:' : 'called from:', frame.trim().split(' ')[1]);
});
}A stack trace is the chain of unfinished calls recorded when the error object was created, newest first, so debugging starts at the topmost frame that lives in code you own.
Worked examples
The top frame can be a built-in
Shows that the first frame may have no file to open, and that a timer callback loses the code that scheduled it.
function scheduleWork() {
setTimeout(runJob, 0);
}
function runJob() {
try {
JSON.parse('{oops}');
} catch (err) {
const frames = err.stack.split('\n').slice(1);
console.log('frame 1:', frames[0].trim().split(' ')[1]);
console.log('frame 2:', frames[1].trim().split(' ')[1]);
console.log('scheduleWork in stack?', err.stack.includes('scheduleWork'));
}
}
scheduleWork();Example explained
Line 1frames[0] is `at JSON.parse (<anonymous>)`: the throw happened inside a built-in, so there is no file:line worth opening there.
Line 2frames[1] names runJob, the first frame you own, and its line:column points at the JSON.parse call that received bad text.
Line 3scheduleWork is absent because setTimeout returned long before the callback ran; the callback began on an empty stack.
Line 4In Node the frames below runJob are node:internal/timers, which is the trace telling you the same thing.
Truncated traces and recursion
Demonstrates how Error.stackTraceLimit decides how many frames survive and what repeated frame names mean.
Error.stackTraceLimit = 3;
function countdown(n) {
if (n === 0) throw new Error('hit zero');
return countdown(n - 1);
}
try {
countdown(50);
} catch (err) {
const frames = err.stack.split('\n').slice(1);
console.log('frames kept:', frames.length);
console.log(frames.map((f) => f.trim().split(' ')[1]).join(' <- '));
}Example explained
Line 1Error.stackTraceLimit caps how many frames V8 records when the error is constructed, and it keeps the innermost ones.
Line 2Only three lines survive out of 51 calls, so the try block that started the recursion is nowhere in the trace.
Line 3Every kept frame has the same name, which is the signature of a recursive chain rather than three different bugs.
Line 4Raising the limit is the fix when the caller you need has been cut off.
What the frame name tells you
Compares the frame text V8 produces for a class method, an object-literal method, and a plain function.
class Cart {
add(item) {
return item.price.toFixed(2);
}
}
const money = {
format(n) {
return n.toFixed(2);
}
};
function plain(n) {
return n.toFixed(2);
}
function topFrame(fn) {
try {
fn();
} catch (err) {
return err.stack.split('\n')[1].trim().split(' ')[1];
}
}
console.log(topFrame(() => new Cart().add({})));
console.log(topFrame(() => money.format(undefined)));
console.log(topFrame(() => plain(null)));Example explained
Line 1Index 1 of the split stack is the first frame; index 0 is the `TypeError: ...` header that V8 puts on top.
Line 2`Cart.add` carries the receiver's type, so a trace tells you which kind of object was running the method.
Line 3`Object.format` is the same pattern for a plain object literal, which is also why CommonJS top-level code shows up as `Object.<anonymous>`.
Line 4`plain` gets no prefix because a bare function call has no meaningful receiver to name.
Important notes
`stack` is not in the language spec. V8 (Chrome, Node, Edge) writes a `Name: message` header then `at fn (file:line:col)` lines; Firefox writes `fn@file:line:col` with no header and has no Error.stackTraceLimit, so code that splits the stack string is engine-specific even though reading it by eye is not.
Line and column numbers refer to the code the engine loaded, not your source: a frame at column 8391 means you are looking at a bundle and need source maps before the numbers point anywhere useful.
Common mistakes
Blaming the topmost frame when it reads `at JSON.parse (<anonymous>)` or `at Array.map (<anonymous>)`, then hunting for a bug in a built-in while the wrong argument is being passed one frame lower.
Reading the trace like a log file with the newest entry at the bottom, which lands you in module top-level or framework bootstrap code that has nothing to fix.
Catching with `console.log(err.message)`, which throws every frame away; the console then shows "Cannot read properties of undefined" with no file, line, or caller.
Try it yourself
Change, predict, then run
In a browser editor, write outer calling middle calling inner, where inner returns `config.retries.toFixed(1)` and config is `{}`, then catch at the top level and log err.stack, confirming the top frame's column lands on `.toFixed`. Now change the call to `setTimeout(inner, 0)` and note exactly which frames disappear from the trace.
Open the JavaScript workspaceCheck your understanding
A trace shows `at Array.map (<anonymous>)`, then `at formatRow (app.js:41:18)`, then `at renderTable (app.js:76:9)`. Where do you start, and why?
- Inside Array.prototype.map, because the topmost frame is always the true fault
- app.js line 76, column 9, because the bottom-most frame is the most recent call
- app.js line 41, column 18, the topmost frame in code you own
- Nowhere useful: an <anonymous> frame means the stack was truncated
Show answer
Frames are recorded innermost first, so map really was running when the error was created, but it is a built-in you cannot change; the callback it invoked is formatRow, making app.js:41:18 the first line you control and the place the bad element was used. Option 1 is tempting if you expect traces to read like a log with the newest line last, but renderTable is only the outer caller that led there, so its line tells you nothing about the bad value.