skip to content

In JavaScript, what is an Error object's `stack` property, when are its frames captured, and why is parsing it fragile?

level: juniorimportance: should knowfreq 58%

answer

  1. a string, not structured data
  2. snapshot taken at one specific moment
  3. construction time, not throw time
  4. engines disagree on the text
  5. frame count is capped by default

basics

~20 s

stack is a string of the frames leading to where the Error was constructed — not where it was thrown. Every major engine provides it, but ECMAScript never specified it, so the exact text differs per engine and parsing it is unreliable.

solid answer

~40 s

`stack` is a non-enumerable own property that every mainstream engine puts on an `Error` instance, holding a human-readable dump of the call frames. The key mechanic is that it is captured when the error is **constructed**, not when it is thrown or caught — so an error object created in a factory and thrown later shows the factory's frames, not the throw site. It is not part of ECMAScript: V8 emits a `Name: message` header line followed by ` at fn (file:line:col)` frames, while other engines use a different shape entirely, so any regex over it is engine-specific and will break. It is also truncated: V8 caps the captured frames at `Error.stackTraceLimit`, which defaults to 10. Treat it as diagnostic text for humans and logs, never as structured data your control flow depends on.

code

javascript · 17 lines
javascript
function make() {
  return new Error('boom'); // frames captured HERE
}

const preMade = make();

function later() {
  throw preMade; // this frame never appears in the trace
}

try {
  later();
} catch (e) {
  console.log(e.stack);
  console.log('enumerable?', Object.keys(e)); // []
  console.log('serialized:', JSON.stringify(e)); // {}
}

go deeper

for a junior

Know that err.stack is a string listing the calls that led to the error, that you log it whole, and that it reflects where the error object was created. Do not promise anything about its exact format.

for a middle

Explain the construction-time capture with an example where a pre-built error is thrown elsewhere, name Error.stackTraceLimit and its default of 10 in V8, and say why the property being non-enumerable breaks naive JSON logging.

for a senior

Show production judgment: constructing errors at the failure site, resisting stack-text parsing for grouping or alerting, and treating the frame limit as a cost/benefit knob on hot error paths rather than a constant to max out.

for a principal

Own the error-diagnostics contract across the estate — what every service guarantees is in a logged error, how traces are grouped without depending on unspecified text, and the budget for capture cost on high-throughput failure paths.

## What the property is When you create an error, the engine records the chain of function calls that led to that point and exposes it as `err.stack`, a string. It is the single most useful field on an error for debugging, and the one most often misunderstood. ```js function inner() { return new Error('boom'); } function outer() { return inner(); } console.log(outer().stack); ``` In a V8 runtime this prints something shaped like: ``` Error: boom at inner (/app/a.js:1:26) at outer (/app/a.js:2:26) at Object.<anonymous> (/app/a.js:3:13) ``` ## It is captured at construction, not at throw This is the point interviewers actually probe. The frames are snapshotted when the `Error` object is built. Throwing it later, catching it, rethrowing it, or storing it in a module-level constant does not re-capture anything: ```js const preMade = new Error('boom'); // frames captured here function later() { throw preMade; } // this frame is NOT in the stack try { later(); } catch (e) { console.log(e.stack); } ``` The trace shows the top-level line where `preMade` was created, and `later` appears nowhere. Two practical consequences follow. Reusing a singleton error object across call sites produces a trace that points at the wrong place forever. And rethrowing a caught error with `throw e` deliberately preserves the *original* capture site — which is usually what you want, and is why rethrowing the same object beats constructing a fresh error that would point at the catch block. (In V8 the *formatting* of the string is lazy — the frames are held structurally and the text is built the first time `.stack` is read — but the frames themselves were fixed at construction. Lazy formatting changes when the cost is paid, not what is recorded.) ## It is not standardized ECMAScript defines `name` and `message` on `Error.prototype`; it does not define `stack`. There is a long-running proposal to standardize *something*, but today the property is a de-facto convention, and its exact text differs by engine: - V8 (Chrome, Node, Edge, Deno) starts with a `Name: message` header line, then indented `at ...` frames. - SpiderMonkey (Firefox) has no header line and uses a `functionName@file:line:column` shape. - JavaScriptCore (Safari) differs again. So `err.stack.split('\n')[1]` means different things in different browsers, and grouping errors in a tracker by a regex over frame text is a portability bug waiting to happen. If you need structured frames, use the tooling in your runtime rather than hand-rolled parsing, and keep the raw string as the fallback. The property is also writable in practice — code can assign to `err.stack`, and some libraries do. Never treat it as a trustworthy record of provenance for anything security-related. ## It is truncated In V8, `Error.stackTraceLimit` controls how many frames are captured; the default is **10**. Deep recursion or a long framework call chain therefore hands you a trace that stops mid-way with no marker that anything was elided. Raising it (`Error.stackTraceLimit = 50`, or `Infinity`) captures more, at a real cost: capturing frames is not free, and on a hot error path — a service that throws thousands of times a second — an unbounded limit is a measurable throughput regression. It is a knob for debugging sessions and for a temporary production investigation, not a set-and-forget value. ## Enumerability and serialization Like `message`, `stack` is non-enumerable, so `JSON.stringify(err)` yields `{}` and object spread drops it. Every logging pipeline needs an explicit error serializer that reads `name`, `message` and `stack` by name. This is the most common reason production logs contain a useless empty object where an error should be. ## How to use it well - Log the whole string; do not slice it to "keep logs tidy" — the frame you cut is the one you needed. - Construct errors at the point of failure so the capture site is the failure site. - Do not branch on stack contents. Branch on error type or on an explicit code property, both of which are stable and cheap to test. - Remember that the string is a snapshot of *your* frames only: asynchronous boundaries and minification both degrade it, and each needs its own remedy.

  • If the stack is captured at construction, what is the practical difference between `throw e` and `throw new Error(e.message)` in a catch block?
    `throw e` propagates the original object, so the trace still points at where the failure was created — the deepest, most useful location. Constructing a new error from just the message captures a fresh stack starting at the catch block and discards the original frames entirely, so the trace now points at your handler rather than at the bug. If you need a new message, keep the original by attaching it as `cause`.
  • What does `Error.stackTraceLimit` control, and why not simply set it to Infinity everywhere?
    In V8 it caps how many frames are captured when an error is constructed; the default is 10. Setting it to `Infinity` gives complete traces but makes every error construction more expensive, which matters on a hot path where errors are thrown at high rate — control flow built on exceptions, or a retry loop. Raise it while investigating, then put it back.
  • Why is grouping errors in a monitoring tool by a regex over `stack` a fragile design?
    The format is unspecified, so the same failure produces different text in Firefox, Safari and Chrome, and a runtime upgrade can change frame naming. Line and column numbers also shift with every deploy, and minified builds change identifiers entirely. Group on a stable error type or explicit code, and use the stack as supporting detail rather than as the key.

saying these in an interview costs you the question

  • Says the stack is captured when the error is thrown
  • Believes ECMAScript specifies the stack format
  • Parses stack text with a regex for control flow
  • Assumes every frame down to entry point is present
  • Expects JSON.stringify of an error to include the stack

context