Skip to content
Anti-Bot

What Is Stack Trace Inspection? What new Error().stack Shows About the Caller

Pim

Pim · Scrappey Research

October 28, 2026 3 min read

What Is Stack Trace Inspection? What new Error().stack Shows About the Caller — conceptual illustration
On this page

Stack trace inspection reads new Error().stack at a chosen moment to learn which engine produced it and which code was on the call path. The stack format is not standardised, so V8, JavaScriptCore and SpiderMonkey each print it differently, down to which column they point at. And because the stack lists every function on the way in, code injected by an automation driver can appear in it if it is on the stack when the sample is taken. Bot-check widgets sample the stack when they are rendered and when their checkbox is clicked.

Quick facts

V8 (Chrome, Edge)Header line "Error", frames "at fn (url:line:col)", 10 frames by default
JavaScriptCore (Safari)No header, frames "fn@url:line:col", column at the opening parenthesis
SpiderMonkey (Firefox)"fn@url:line:col", eval frames marked "line N > eval"
Sampled atWidget render and checkbox click in one deployed widget
Different fromStack depth fingerprinting, which measures the maximum call depth

What a stack string contains

When a script creates an Error, the engine records the call stack and exposes it as a string. The engines differ in almost every detail:

EngineFrame formatDetails
V8 at Object.render (https://a.example/app.js:12:9)First line Error; methods named by call form; Error.stackTraceLimit 10; URL keeps the query
JavaScriptCorerender@https://a.example/app.js:12:15No header; named by definition; query and fragment stripped; strict-mode tail calls can drop frames
SpiderMonkeyrender@https://a.example/app.js:12:9Eval and Function frames marked as line N > eval

A single stack string therefore names the engine, and the exact column numbers show which engine's position rules produced it. A User-Agent override cannot change how V8 prints a frame. This is one of the checks behind engine oracle fingerprinting; it is a different signal from stack depth fingerprinting, which measures how deep recursion can go.

What automation leaves in stacks

Measured in Chrome 156, code run by common drivers produces distinctive frames:

  • Puppeteer page.evaluate: frames at pptr:evaluate;%2Fpath%2Fto%2Fscript.js%3A20%3A33:1:8. Puppeteer appends a //# sourceURL= comment, and the URL encodes the driver script's absolute path, line and column on the controlling machine.
  • Playwright page.evaluate: at UtilityScript.evaluate (<anonymous>:372:16) and an eval at evaluate frame, because Playwright runs user code through an evaluator class.
  • Raw protocol evaluation and init scripts: frames like at <anonymous>:1:35 with no URL. Real page scripts always have a URL.

These frames only show up if the injected code is on the stack when a sample is taken. Timers and event callbacks start with a fresh stack: a setTimeout callback had exactly one frame in testing.

When widgets sample the stack

One widely deployed challenge loader records new Error().stack during its render call. If the widget is declared in the page's markup, the loader renders it from its own DOM-ready handler and the stack contains only loader frames. If a page script calls the render function, the stack contains that script's URL, line, column and function names, and if an evaluated driver script makes the call, the driver's frames are recorded too. See what a challenge widget collects.

The same widget samples the stack again inside its checkbox click handler. That stack has to look like the claimed engine's: V8 names the listener frame HTMLInputElement.<anonymous>, JavaScriptCore leaves many frames anonymous. A Safari User-Agent with a V8-shaped stack contradicts the session.

Two practical points follow: let the page render its own widgets instead of calling page APIs from injected code, and run the engine you claim, because the stack format belongs to the engine. A managed web data API that drives real browsers without injecting page-world code avoids both problems.

Code example

javascript
// See your own engine's stack format and what appears on the call path.
function outer() { return inner(); }
function inner() { return new Error().stack; }

console.log(outer());
// Chrome:  "Error\n    at inner (<url>:3:27)\n    at outer (<url>:2:27)\n ..."
// Safari:  "inner@<url>:3:36\nouter@<url>:2:31\n..."
// Firefox: "inner@<url>:3:27\nouter@<url>:2:27\n..."

// A timer callback starts with a fresh stack: the scheduler is not recorded.
setTimeout(() => console.log(new Error().stack.split('\n').length), 0);

// Frames with no URL come from evaluated code (devtools console, CDP
// Runtime.evaluate, init scripts). Real page scripts always have one.
const noUrl = new Error().stack.split('\n').filter(l => /<anonymous>:\d+:\d+/.test(l));
console.log('URL-less frames:', noUrl.length);

Related terms

Concept map

Concept map

How Stack Trace Inspection connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Can a website see that my code ran through Puppeteer or Playwright?

It can if that code is on the call stack when the page creates an Error. Puppeteer's evaluated code carries a pptr:evaluate URL that encodes the driver script's path, and Playwright's carries UtilityScript frames. Code that only reads the page and never calls page functions does not appear in the page's own stacks.

Why do Chrome and Safari stack traces look different?

The format of Error.stack is not standardised. V8 prints a header line and "at fn (url:line:col)" frames, while JavaScriptCore prints "fn@url:line:col" with no header and points columns at the opening parenthesis of the call.

Can I change the stack format to match another browser?

Not reliably from JavaScript. Stacks are produced by the engine for every error, including errors created inside cross-origin frames and workers that page-level patches do not reach. The format can only be right if the engine is the one the session claims.

Last updated: 2026-10-28 · Facts last verified: 2026-10-28