edgegoat Guides
Negative zero in JavaScript: why -0 is different from 0
IEEE 754 numbers carry a sign bit even for zero. JavaScript hides it in most places and exposes it in a few, and that inconsistency is exactly what turns into a bug.
Where -0 comes from
- Rounding small negatives:
Math.round(-0.4),Math.trunc(-0.5),Math.ceil(-0.5). - Arithmetic:
-1 % 1,-0.5 * 0,0 / -5. - Parsing:
Number("-0")andJSON.parse("-0").
Where it shows up
1 / -0is-InfinityandMath.atan2(0, -0)is pi.Object.is(-0, 0)is false, so React state, memoization andObject.isbased equality treat them as different.String(-0)andJSON.stringify(-0)print"0", butIntl.NumberFormatandtoLocaleString()print"-0".
The bug: "-0" on screen
A change indicator rounds a tiny negative delta to zero, and Intl.NumberFormat keeps the sign. Users see -0.
function formatDelta(n) {
return (n > 0 ? '+' : '') + new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 }).format(n);
}
formatDelta(-0.04); // "-0"The edgegoat test that catches it
import { expect, test } from 'vitest';
import { numbers } from 'edgegoat';
test.each(numbers({ tags: ['zero', 'sign'] }))('$label', ({ value }) => {
expect(formatDelta(value)).not.toBe('-0');
});negative zero and -0.0001. The fix is signDisplay: 'exceptZero', which prints +0.5, -0.5 and a plain 0.FAQ
Floating point numbers store a sign bit separately from the magnitude, so zero can be positive or negative. === treats them as equal, but division, Object.is, Math.atan2 and Intl formatting can tell them apart.
Use Object.is(x, -0), or x === 0 && 1 / x === -Infinity in older code.
JSON.parse("-0") returns -0, but JSON.stringify(-0) returns "0", so a round trip loses the sign.
Add zero (x + 0) or write x === 0 ? 0 : x. For display, Intl.NumberFormat with signDisplay: 'exceptZero' or 'negative' avoids printing it.