Why NaN === NaN is false: how ===, Object.is and includes compare values in JavaScript
In JavaScript, NaN === NaN is false but [NaN].includes(NaN) is true. The ECMA-262 rules for how ===, Object.is, and includes, Map and Set treat NaN and -0, with code.
NaN === NaN is false. Yet [NaN].includes(NaN) is true, and [NaN].indexOf(NaN) is -1. It looks like a bug, but all three follow the specification. ECMA-262, the JavaScript standard, defines more than one algorithm for deciding whether two values are “the same”, and different operators and methods use different ones.
The spec text below is from the current ECMA-262 draft, dated October 3, 2026. The code results were checked by running them in Node.js 24.13.0.
Only two comparisons differ
The spec has three comparison algorithms: IsStrictlyEqual, SameValue and SameValueZero. All three first look at the types of the two values and return false straight away if they differ. For anything that isn’t a number, all three go through the same steps: strings are equal if they have the same code units in the same order, and objects are equal only if they are the same object.
The three algorithms give different answers in only two cases: NaN compared with NaN, and +0 compared with -0. Every other pair of numbers, such as NaN and 1, gets the same answer from all three.
| Algorithm | NaN vs NaN |
+0 vs -0 |
Used by |
|---|---|---|---|
| IsStrictlyEqual | different | equal | ===, indexOf, lastIndexOf, case in switch |
| SameValue | equal | different | Object.is |
| SameValueZero | equal | equal | includes, Map keys, Set values |
When both values have the same type, == gives the same result as ===. That is why NaN == NaN is false too.
Why NaN isn’t equal to itself
The numeric comparison performed by === (Number::equal) begins with two steps: if x is NaN, return false; if y is NaN, return false. Whatever the other side is, even the same NaN, the answer is false whenever one side is NaN.
A JavaScript Number is an IEEE 754 double-precision (binary64) value. IEEE 754 has many bit patterns for NaN, but ECMA-262 treats them all as a single NaN value and says JavaScript code cannot tell them apart. Because there is only one NaN, SameValue and SameValueZero can treat NaN as equal to NaN.
NaN === NaN; // false
Object.is(NaN, NaN); // true
[NaN].indexOf(NaN); // -1
[NaN].includes(NaN); // true
new Set([NaN, NaN]).size; // 1
If you need the position of a NaN in an array, use findIndex(Number.isNaN) instead of indexOf. Checking for NaN also needs the right function. The global isNaN converts its argument to a number first, so isNaN("abc") is true. Number.isNaN doesn’t convert, so it returns false for the same input. The spec also notes that x !== x is true only when x is NaN.
Where -0 comes from, and why it’s easy to miss
Number has a separate +0 and -0. === treats them as equal; Object.is treats them as different.
One reason -0 is easy to miss is string conversion. The rule for turning a number into a string (Number::toString) returns "0" for both +0 and -0, so String(r) and log messages built by string concatenation show -0 as 0. Developer consoles, which display the value itself, may show -0.
const r = Math.round(-0.4);
String(r); // "0"
r === 0; // true
Object.is(r, -0); // true
1 / r; // -Infinity
Math.round is specified to return -0 for values that are at least -0.5 and less than 0. Dividing a positive number by -0 gives -Infinity, so once such a value reaches a division, the sign shows up in the result. When you need to tell +0 from -0, use Object.is.
includes, Map and Set treat both zeros as one
Array.prototype.includes compares with SameValueZero. The spec spells out two ways it differs from indexOf: it can find NaN, and it doesn’t skip empty array slots but treats them as undefined.
[-0].includes(0); // true
[, 1].includes(undefined); // true
[, 1].indexOf(undefined); // -1
At the start of their sections, the spec says that Map keys and Set values are distinguished using SameValueZero semantics. The algorithms themselves are written so that a -0 is first turned into +0 and then compared with SameValue. So a key you add as -0 comes back out as +0.
const m = new Map([[-0, "zero"]]);
m.get(0); // "zero"
Object.is([...m.keys()][0], -0); // false
Which comparison to use
- Ordinary comparisons:
=== - Checking whether a value is NaN:
Number.isNaN(x) - Whether an array contains NaN:
includes; for its position,findIndex(Number.isNaN) - Telling +0 from -0:
Object.is
For objects, every method only checks whether both sides are the same object. Even with identical contents, Object.is({}, {}) is false.