The Rounding Your Browser Was Allowed to Get Wrong
There is a sentence in the JavaScript standard that says nobody has to get this right. It has been there since the first edition, in 1997, it applies to Math.exp and Math.sin and twenty-one other functions, and the consequence is running on your machine now: your browser hands back numbers whose last bit is wrong, and how often depends entirely on which maths library is underneath it. The engines here that use fdlibm, the library the standard itself recommends, miss … of exponentials. JavaScriptCore and glibc miss … of the same list. This page builds the correct answer from scratch, in BigInt, in front of you, and measures your own engine against it. Five engines were measured on one machine at one instant. Yours is the sixth column.
Here is the sentence. It is a Note in clause 21.3.2 of ECMA-262, and this page found it word for word in each of the three editions it fetched, and again in the first edition of 1997, where it sat in clause 15.8.2 and named ten functions instead of the current twenty-three.
…
…
Some latitude is allowed. Not an error bound, not a tolerance, not a maximum. Latitude. One name in that list is worth pointing at before it looks like a typing error: Math.random is on it, where being unspecified is the whole point rather than a concession, and this page measures none of it. The other twenty-two are functions with a right answer. Elsewhere the standard gives the class of thing this is a name, and is candid about what the name buys you:
…
…
Encouraged to strive. That is the whole of your guarantee about the exponential function.
1. Your engine, against the truth
To say an engine is wrong you need something to be wrong against, and the something cannot be another engine. So the page carries its own arbiter: a small binary floating-point arithmetic in BigInt, with the working precision under your control, which computes the true value to a few hundred bits and then rounds it once, correctly. Nothing in it is a stored constant. Pi and the natural logarithm of two are summed on demand to whatever precision you ask for.
Type an argument. The row marked correctly rounded is what the standard would say if the standard said anything.
Instrument 1 · one argument, sixty-four bits
Shows your engine's result and the correctly-rounded result for the chosen function at the chosen argument, as decimal and as 64 bits, with the differing bits highlighted.
Drag the precision down and the reference starts refusing to answer. That refusal is section 5.
Press find one and the page walks the argument list until it finds one your engine gets wrong. On the engines measured here it does not have to walk far.
2. The census: how often, and by how much
One argument proves nothing about a rate. The page ships a fixed list of 1,000 arguments per function, generated by a 48-bit linear congruential generator with a seed written down before any measurement was taken, so that every engine that has ever been pointed at this page saw byte-identical inputs. Press the button and your machine computes the correctly-rounded value for all of them and counts how often it disagrees with itself.
Instrument 2 · the census
Nothing measured yet. Your column is empty until you press a button.
Percentage of arguments on which each engine returned something other than the correctly-rounded double. The five leftmost columns were captured on one Linux machine on the date in the apparatus; the rightmost is yours, now. The final column is the published maximum error for the same function from the glibc manual, in real ulps, which is not the same unit; section 4 is about that.
Two things in that table are worth stopping on, and neither is the headline.
The first is the row for sqrt, which is 0.0% in every measured column, and will be in yours the moment you press the button. That row is the reason to believe the others. If a measurement apparatus reports a divergence everywhere it looks, it is measuring itself. A row that comes back exactly zero, on the one function of the eight where zero is the right answer, is the thing that says the other seven rows are about the engines and not about the page. Section 3 is about why that row is zero, and it is a better story than it looks.
The second is that the columns fall into camps, the camps are not the ones a reader would guess, and on two of the eight functions they are not camps at all. …
3. The one that got pinned
In the fifteenth edition of ECMA-262, published June 2024, Math.sqrt was on the latitude list with the rest of them, and clause 21.3.2.32 ended like this:
…
…
In the sixteenth edition, published June 2025, the same step reads:
…
…
The blackboard F is the standard's own operator for the Number value for, and the standard is explicit that it means round to nearest, ties to even. In one line, Math.sqrt stopped being approximated and started being specified, and its name was struck out of the list of functions in clause 21.3.2 whose behaviour is not precisely specified. It is the only name that has ever left that list.
The order of events is the other way round, and it matters. The zero row is not the standard's doing. Square root is one of the operations IEEE 754 has required to be correctly rounded since 1985, and on x86-64, the architecture every column here was captured on, it is a single machine instruction; the engines were returning the correctly-rounded double for years before anyone changed a word of ECMA-262. What the pull request titled Normative: fully define Math.sqrt, opened on 6 June 2024 and merged on 17 August 2024, actually did was stop permitting anything else, and its entire stated rationale was that the engines were all doing it already. The difference between the 0.0% row and the 9.96% row above it is not one sentence in a document. It is that one of the two has a hardware guarantee underneath it and the other has a recommendation, and the document has now caught up with exactly one of them.
Instrument 3 · the latitude list, three editions
Every function named in the Note to clause 21.3.2 as not precisely specified, in each of the three most recent editions of ECMA-262. Clause numbers are read out of the published document by 00-spec-quotes.mjs, because they move: sqrt is 21.3.2.32 in the 15th edition and 21.3.2.33 in the 16th and 17th, since a clause was inserted above it.
function
ES2024 · 15th
ES2025 · 16th
ES2026 · 17th
your engine, measured
4. Your number, and the published number
The GNU C library publishes a table of the largest error it knows of in each of its maths functions, per architecture, in units of the last place. It is the closest thing to a published accuracy figure that any of the libraries under this page has, and one of the five engines measured here is that library, reached through python3 rather than through a browser, on the same machine in the same minute. So the comparison is available directly.
Six of the eight functions carry a published number. On every one of those six the measured maximum is inside the published bound. Two carry no published number at all, and one of those two is not zero, which is the interesting row.
Instrument 4 · measured against published
The glibc manual's known maximum error for x86_64, against what a thousand and then twenty thousand random arguments actually found in the copy of glibc installed on the capture machine. The two columns are in different units and the mismatch is deliberate: see below.
function
published, ulps
measured, wrong on
measured, whole doubles
real error implied
The units are not the same, and pretending otherwise would be the easiest lie on this page. The published figure is a real number of ulps: an error of 0.9 ulp is reported as 1. What this page measures is how many representable doubles apart two answers are, which is an integer. An integer distance of 1 means the real error lay somewhere in the half-open range above half an ulp and up to one and a half. So a measured 1 and a published 1 are consistent, and a measured 3 against a published 4 is consistent, and neither is a coincidence to be celebrated.
…
The manual says the rest itself, and it is worth reading as a sentence about how published error bounds are made rather than as an excuse:
…
…
That is the honest shape of the gap between your number and the published number, everywhere on this page. The published number is what somebody's test suite found. Yours is what a thousand arguments drawn from a seed found in your browser, thirty seconds ago. Neither is the maximum. The maximum is not known for any of these functions, which is section 5.
5. The arguments nobody can round
The reference on this page rounds a few hundred bits of the true value down to fifty-three. That works because the bits it discards almost always decide the matter: the true value sits clearly nearer one double than the other. Almost always.
Sometimes the true value sits so close to the exact midpoint between two doubles that a few hundred bits cannot tell which side it is on. There is no clever fix. You compute more bits, and either that settles it or it does not, and nobody can tell you in advance how many will be enough. This is the Table Maker's Dilemma, and it is why correctly rounded is a hard property to ship rather than an obvious one.
It is also measurable, and the measurement has a shape you can predict before you take it. For a random argument, the bits of the true value past the point where the double stops behave like a fair coin. The chance that the first k of them are all the same, which is what it takes to sit within 2^-k of the midpoint, halves with every extra bit. So the count of arguments at each margin should fall by half, step after step, and the reference's refusal rate at a given working precision is the tail of that same fall.
Instrument 5 · the dilemma, and the refusal
Nothing measured yet.
Gold is what your browser counted. Blue is N / 2^(k-1), the prediction, which was not fitted to anything.
At low working precision the reference declines to name a value. This measures how often, at each precision.
How often the reference refuses, against its working precision, in your browser. A refusal is not an error and is never counted as agreement: those arguments are removed from the census denominator and reported separately.
working bits
refused
rate
expected from the halving law
…
6. One bit becomes the whole trajectory
A wrong last bit is a relative error of about one part in nine quadrillion, and for most purposes that is the end of the discussion. Not for all of them. Feed the two answers into a system that amplifies differences and the difference stops being small on a schedule you can compute in advance.
The map used here is x → 4x(1-x), chosen because its rate of amplification is not a fitted number but an exactly known one. Substituting x = sin²(πt/2) turns it into t → 2t mod 1, whose derivative is 2 wherever it exists; a change of variable cannot change a Lyapunov exponent; so the exponent is ln 2, exactly. One bit of separation is lost per step. Two trajectories that begin one double apart should therefore be visibly different after about 48.7 steps, and that number is arithmetic, not a fit.
The seeds below are not hypothetical. They are the values two real browsers actually returned for Math.exp, captured on one machine, at the arguments where they disagreed.
Instrument 6 · fifty steps
Two trajectories from two engines' answers to the same question. They are drawn on top of each other and stay that way, until they do not.
…
And sometimes the opposite happens, which is the half of this that a demonstration would leave out. The map contracts wherever the absolute value of 4 - 8x is below one, which is a quarter of the interval, and a difference of a single double can be rounded out of existence there. When that happens the two engines agree from that step onward, forever, and there is no divergence time to print. … The page prints which of the two happened rather than a number in both cases.
7. What your engine tells a stranger
If the answers differ by engine, the answers are a name badge. This is not a hypothetical: it is the stated reason Firefox changed its arithmetic. In July 2021 Tom Ritter posted an intent to implement to mozilla.dev.platform under the subject Intent to Implement: Use fdlibm for Math.cos, Math.sin, and Math.tan to prevent math-based fingerprinting, and the bug it closed, 531915, Floating point differences between platforms, had been open since 30 November 2009. It was resolved fixed on 13 August 2021, against milestone 93.
Here is what your engine discloses through this door, and here is the honest accounting of how much that is.
Instrument 7 · your signature
Ten expressions, evaluated in your browser now, beside the exact bits five other engines returned for the same expressions on one Linux machine. A cell that matches yours is dimmed. The point of the table is which rows separate engines and which do not.
The row worth looking at is the first one. Math.pow(Math.PI, -100) returns one of exactly two doubles, and both of them are written out by name in a comment in Firefox's own source, in js/src/builtin/Math.cpp, where a function called pow_libm_or_fdlibm decides which library to call:
…
…
…
…
So the honest version of the fingerprinting claim, in 2026, is narrow and specific: a Firefox on Windows still computes sine with whatever Microsoft ships, and a Firefox anywhere else does not. The capture machine here runs Linux, so every trace on this page is from the fdlibm side of that switch and this page cannot show you the Windows side. If you are reading this on Firefox for Windows, your sin column is the one measurement on this page that nobody else here has.
What the check panel checks
Every number above that came from the thousand-argument sample is recomputed below, in your browser, from the reference engine and the committed traces. The traces are not shipped raw: the page carries the reference table and the 1,812 places where the five engines depart from it, in place of 40,000 doubles, and rebuilds each engine's full run from those departures. The check panel hashes each rebuilt run and compares it against a digest taken from the raw captured file, so a lossy or drifted encoding cannot pass quietly.
The figures marked OFFLINE are the exception, and it is worth being exact about what the mark means. The twenty-thousand-argument census is half a minute of BigInt and no browser is going to redo it while you wait, so the page reads its counts from a file. A file is not a measurement. What travels with that file is the list of every argument at which each engine departed from the reference, and the panel below requires all forty counts to equal the length of the list behind them, which is the check that was missing when an audit forged two of those counts and every green light on this page stayed green. The lists themselves are re-derived offline, against a reference recomputed from scratch, by research/the-rounding-you-were-allowed/verify.mjs, which also re-runs glibc over all twenty thousand arguments and requires the departures to come back identical.
Three of the checks are controls that must come back dirty, and they matter more than the ones that come back clean. A reduction that cannot fail a deliberately corrupted trace has not proved anything by passing a real one.