How sites tell V8, SpiderMonkey and JavaScriptCore apart: stack traces, error text, toString output and Math precision, and what the signal can prove.
A user-agent string is a declaration. The JavaScript engine is the thing actually executing the page, and its implementation details leak through ordinary, standard APIs. That is what makes JavaScript-engine signals useful: they are not a high-entropy identifier that follows you around, they are a lie detector for the identity a browser claims. If you need background on what a JavaScript engine is and how it pairs with a rendering engine, start with Rendering Engine vs JavaScript Engine; this post goes straight to the techniques.
Key Takeaways
- Three engines cover almost every real browser: V8 (Chrome, Edge and other Chromium browsers), SpiderMonkey (Firefox) and JavaScriptCore (Safari, and in practice nearly every browser on iOS).
- Engine tells come from places the language specification leaves open: the format of stack traces, the wording of built-in error messages, the text of native-function source, and the precision of some
Mathfunctions. - The signal is coarse. It sorts a visitor into a handful of buckets (plus version-era differences), so on its own it identifies an engine, not a person.
- Its real value is consistency checking: a browser that claims to be Safari but executes like V8 has told the page two different stories.
- BrowserInsight's own consistency check falls back to the
Error().stackformat when other engine signals are inconclusive.
Why the Engine Gives Itself Away
The ECMAScript specification defines what the language must do, not how every observable detail must look. Where the spec is silent or explicitly implementation-defined, each engine made its own choice, and those choices have been stable for years because websites came to depend on them. Nothing here requires special permissions: a script reads a string, compares it with what each engine is known to produce, and gets an answer.
Technique 1: The Shape of Error.stack
Error.prototype.stack is not part of the standard, which is exactly why it differs. V8 starts the string with the error's name and message and writes frames as at functionName (url:line:column). SpiderMonkey and JavaScriptCore have no such header line and write functionName@url:line:column. The two non-V8 engines can be told apart from each other by smaller details such as how anonymous frames and column numbers are written, which is why those details are version-sensitive and worth re-checking rather than hard-coding.
V8 also originated two companion APIs: Error.captureStackTrace and Error.stackTraceLimit. MDN documents captureStackTrace as non-standard, and its compatibility table is the place to check which engines have since added it. Presence or absence of such V8-flavored extras is a second, independent hint.
Technique 2: Built-in Error Message Text
Trigger the same mistake in three engines (calling something that is not a function, reading a property of null) and you get three different sentences, because the specification defines the error type but not the message. MDN's JavaScript error reference lists many errors with the per-engine message variants side by side. A page can catch the exception, read error.message, and match the wording against a small lookup table. Message text also shifts across versions of the same engine, so a careful check treats it as evidence for an engine family, not an exact version.
Technique 3: Function.prototype.toString on Native Functions
Calling Function.prototype.toString on a built-in such as Array.prototype.push returns text of the form function push() { [native code] }. Modern specifications pin that general shape, but they still leave room for whitespace and formatting differences, and the resulting string and its length have historically varied between engines. A script can compare the output for a few built-ins against what the claimed browser should produce. This probe also has a second use: a function that has been replaced by a JavaScript wrapper stops printing [native code], which is why lie-detection tools lean on it.
Technique 4: Math Precision at the Edges
The ECMAScript spec requires only that functions such as Math.sin, Math.exp or Math.pow return an implementation-approximated result. MDN's Math reference notes that the precision of many of these functions is implementation-dependent. Engines, and sometimes CPU or operating-system math libraries, can therefore disagree in the last digits for unusual arguments. Because that last-digit behavior depends on the math library as well as the engine, treat it as a supporting signal that clusters visitors, not as a clean per-engine label. We do not list specific digits here; they change with versions and platforms.
Technique 5: Recursion Limits
Recurse until the engine gives up and catch what it throws. V8 and JavaScriptCore throw a RangeError saying the maximum call stack size was exceeded; SpiderMonkey throws its non-standard InternalError with the message "too much recursion" (see MDN's too much recursion entry). Both the error type and the wording are coarse engine tells. The depth reached before the error varies with frame size, stack size and the device, so it is better read as a rough hint about the build and environment than as an exact engine identifier, and it is the least stable probe in this list.
Why This Is Hard to Fake
A Chromium-based anti-detect browser can rewrite the user-agent, Client Hints and navigator properties, but it still runs V8. Making V8 produce SpiderMonkey's or JavaScriptCore's stack format, error wording, toString text and Math output means patching a large number of independent behaviors, and every patched built-in is itself a place where a prototype-level change can show up. That is why engine checks pair well with the lie-detection logic in CreepJS-style analysis, and why the gap between a patched profile and a genuine browser, covered in anti-detect browsers vs real browsers, tends to be found in cross-signal inconsistency rather than any single field. This post describes detection; it is not a guide to defeating it.
Where This Is Used
The legitimate use is consistency checking. Bot and fraud systems compare the browser a session says it is (see detecting user-agent spoofing) against the engine that is actually executing, as one input among many in bot detection and headless browser detection. The same idea works at a coarser level for version ranges: feature-based version detection brackets how new an engine is, while the techniques above establish which engine it is.
What BrowserInsight Actually Checks
Two things, stated plainly. The consistency check behind /fingerprint-check, /bot-detection and /vpn-check falls back to the Error().stack format to classify the engine when other signals are inconclusive. The kernel check separately estimates the engine version from feature support. We do not probe error-message wording, Math precision or toString length, and everything runs in your browser; nothing is sent to a server.
Frequently Asked Questions
Can a website identify me from my JavaScript engine? Not on its own. The engine splits visitors into a few large groups, so it adds very little to a unique fingerprint. Its value is in checking whether your other claims are consistent.
Does a different JavaScript engine mean a different browser? Usually. Chrome, Edge and Brave all run V8, Firefox runs SpiderMonkey, and Safari and nearly all iOS browsers run JavaScriptCore. Browsers sharing a family are indistinguishable by engine alone.
Are these differences permanent? No. Engines change message wording, stack formats and math implementations between releases, and standards work can remove some differences. Any detection built on them needs ongoing maintenance.
Is Error.stack standard?
It is widely implemented but non-standard, as MDN documents, which is why its format is a useful engine signal.
Conclusion
JavaScript-engine fingerprinting is a small, honest signal: a handful of engine buckets read from stack traces, error text, native-function source, Math results and recursion limits. It cannot single you out, but it can show that a browser's description and its behavior disagree. To see your own engine as the page sees it, run the fingerprint check or the kernel check.

