How does a value in JavaScript end up as -0, and how do you detect it given that -0 === 0 is true?
answer
- the sign bit survives a zero magnitude
- === treats both zeros as equal
- string and JSON output hide it
- take the reciprocal to see the sign
- Object.is separates the two zeros
basics
~20 sIEEE-754 keeps a sign bit even when the magnitude is zero, so 0 * -5, Math.round(-0.4) and underflow all give -0. Strict equality cannot see it; use Object.is(x, -0) or check that 1 / x is -Infinity.
solid answer
~40 sJavaScript numbers are IEEE-754 doubles, which store a sign bit independently of the magnitude — so zero comes in two flavours, `+0` and `-0`. You get `-0` from a negative literal, from a product or quotient whose mathematical result is zero but whose sign is negative (`0 * -5`, `-1 / Infinity`), from rounding a small negative number (`Math.round(-0.4)`), from underflow (`-1e-400`), and from `Math.sign(-0)` or `Math.min(0, -0)`. Almost nothing reveals it: `-0 === 0` is true, `-0 < 0` is false, `String(-0)` is `"0"`, and `JSON.stringify(-0)` is `"0"`. Two reliable detectors: `Object.is(x, -0)`, and the reciprocal trick `1 / x === -Infinity`, since dividing by negative zero gives `-Infinity`. It matters mainly where you later divide by the value or key on its sign.
code
javascript · 11 linesconst z = 0 * -5;
console.log(z === 0); // true — === cannot see the sign
console.log(Object.is(z, -0)); // true — SameValue can
console.log(1 / z); // -Infinity — the reciprocal trick
console.log(String(z)); // "0"
console.log(JSON.stringify(z)); // "0"
console.log(Math.round(-0.4)); // -0
console.log(Math.min(0, -0), Math.max(0, -0)); // -0 0
console.log([z].includes(0)); // true — SameValueZero ignores the signgo deeper
Know that JavaScript has both +0 and -0, that === reports them equal, and that Object.is(x, -0) is the way to tell them apart. Recognising -0 in a console log is enough at this level.
Explain that the IEEE-754 sign bit is independent of the magnitude, name concrete producers such as 0 * -5 or Math.round(-0.4), and explain why 1 / x reveals the sign.
Point at real consequences: a division that flips between Infinity and -Infinity, or a change detector using Object.is that fires on a 0 to -0 flip, and normalise the value at the boundary instead.
Decide the codebase convention: whether -0 is ever meaningful in your domain, where normalisation happens, and how numeric invariants are asserted so the two zeros never diverge across a serialization boundary.
## Why there are two zeros Every JavaScript number is an IEEE-754 double-precision float, and that format stores the sign in its own bit, separate from the magnitude. When the magnitude bits are all zero, the sign bit is still there — so the format can represent both `+0` and `-0`. JavaScript exposes both, and the literal `-0` produces the negative one. The standard keeps `-0` so that the sign of a result that underflows to zero is not lost: `-1e-400` is too small for a double, and the nearest representable value is `-0` rather than `+0`. Some numerical code relies on that preserved sign, notably around branch cuts and reciprocals. ## Where -0 comes from in ordinary code ```js 0 * -5; // -0 — sign of the product is negative -1 / Infinity; // -0 Math.round(-0.4); // -0 Math.round(-0.5); // -0 (rounds toward +Infinity) Math.sign(-0); // -0 Math.min(0, -0); // -0 parseFloat("-0"); // -0 -1e-400; // -0 — underflow keeps the sign ``` Note the asymmetry: `Math.min(0, -0)` is `-0` while `Math.max(0, -0)` is `+0`, exactly as the spec requires. `Math.abs(-0)` gives `+0`. Realistically, `-0` shows up in geometry and animation code (a delta multiplied by a negative scale), in aggregation over signed values, and anywhere `Math.round`/`Math.sign` is applied to small negatives. ## Why you almost never notice Strict equality uses IEEE-754 numeric comparison, which treats `+0` and `-0` as numerically equal. So do the relational operators: ```js -0 === 0; // true -0 == 0; // true -0 < 0; // false -0 <= 0; // true ``` String conversion also hides it — `String(-0)` and `` `${-0}` `` are both `"0"`, and `(-0).toFixed(2)` is `"0.00"`, not `"-0.00"`. (A displayed `"-0.00"` almost always comes from a small negative value like `-0.001` rounding, not from a true `-0`.) `JSON.stringify(-0)` produces `0`, so the sign never survives a round trip through JSON. Even the collections normalise it: `Set.prototype.add` and `Map.prototype.set` convert a `-0` key to `+0` before storing, and `[-0].includes(0)` is true because `includes` uses SameValueZero, which ignores the difference. ## Detecting it Two reliable checks: ```js function isNegativeZero(x) { return Object.is(x, -0); } function isNegativeZeroReciprocal(x) { return x === 0 && 1 / x === -Infinity; } ``` `Object.is` implements the SameValue rule, which is strict equality except that it distinguishes `+0` from `-0` and treats NaN as the same as NaN. The reciprocal trick predates it: dividing by `+0` gives `Infinity`, dividing by `-0` gives `-Infinity`, and that is the sharpest observable difference between the two zeros. What does **not** work: `x === -0` is true for `+0` as well, `x < 0` is false, and `Math.sign(x) === -1` is false because `Math.sign(-0)` returns `-0`, not `-1`. ## When it actually matters Three situations: 1. **You divide by the value.** A rate computed as `distance / elapsed` flips from `Infinity` to `-Infinity` depending on which zero `elapsed` happens to be, and downstream comparisons or clamps then behave in opposite directions. 2. **You branch on sign.** Code that asks "was this movement negative?" via `1 / delta < 0` gets a different answer than code that asks via `delta < 0`. Pick one convention. 3. **You compare snapshots for change detection.** If a memoisation or dirty-check layer compares with `Object.is` — which is the SameValue rule — then `+0` and `-0` count as *different*, and a value that flipped from `0` to `-0` triggers work even though nothing visibly changed. The usual fix is to normalise on the way in: `const clean = value === 0 ? 0 : value;` turns any `-0` into `+0`, because the comparison is numeric. For most application code the right answer is to normalise early rather than to propagate two zeros through the system and hope every comparison agrees.
- Why does the reciprocal trick work as a -0 detector?Division by zero is defined by IEEE-754 to produce an infinity carrying the combined sign of the operands. `1 / +0` is `Infinity` and `1 / -0` is `-Infinity`, so the reciprocal turns an invisible sign bit into two values that strict equality can tell apart. Guard with `x === 0` first, otherwise any negative number also gives a negative reciprocal.
- If a value might be -0 and you do not want it, how do you normalise it?Add zero-checked reassignment: `const clean = value === 0 ? 0 : value;`. Because `-0 === 0` is true, the branch catches both zeros and the literal `0` on the right replaces the sign. Alternatively `value + 0` leaves `-0` unchanged, so do not reach for that; `Math.abs` works only when you are certain the value is a zero.
- Does putting -0 in a Set or using it as a Map key preserve the sign?No. Both normalise it: `Set.prototype.add` and `Map.prototype.set` convert a `-0` argument to `+0` before storing, and lookups use SameValueZero, which does not distinguish the zeros anyway. So `new Set([0, -0])` has size 1 and the stored element is `+0`. If the sign matters, keep it outside the key.
saying these in an interview costs you the question
- Claims -0 does not exist because -0 === 0 is true
- Detects it with x < 0 or Math.sign(x) === -1
- Expects String(-0) or JSON.stringify(-0) to show a minus sign
- Thinks Object.is is just an alias for strict equality
- Assumes a displayed "-0.00" must come from a real -0