JavaScript indexOf Cannot Find NaN: Check includes and Sparse Slots
Contents
[NaN].indexOf(NaN) returns -1 even though the array contains NaN. Use includes(NaN) when you need a Boolean answer. If you need its position, use findIndex(Number.isNaN). Before replacing every indexOf(value) !== -1 check, test your handling of empty slots: Array(1).includes(undefined) is true, while Array(1).indexOf(undefined) is -1.
The distinction matters when a numeric pipeline can produce NaN, or an array contains holes rather than explicit values. The following fixture verifies both boundaries and includes an explicit-value search that excludes holes. It covers ordinary arrays without indexed prototype properties; proxies, getters, typed arrays, and concurrent mutation are outside its scope.
Reproduce the membership differences
Save this as membership.mjs and run node membership.mjs. The assertions were executed during article preparation on October 24, 2026, using Node.js 24.13.0 on macOS. They require no packages.
import assert from 'node:assert/strict';
const values = [10, NaN, 20];assert.equal(values.indexOf(NaN), -1);assert.equal(values.includes(NaN), true);assert.equal(values.findIndex(Number.isNaN), 1);console.log('NaN: indexOf=-1, includes=true, findIndex=1');
const holes = Array(1);assert.equal(Object.hasOwn(holes, 0), false);assert.equal(holes.indexOf(undefined), -1);assert.equal(holes.includes(undefined), true);const explicit = [undefined];assert.equal(Object.hasOwn(explicit, 0), true);assert.equal(explicit.indexOf(undefined), 0);assert.equal(explicit.includes(undefined), true);console.log('undefined: hole differs, explicit entry matches both');
function findOwnValue(array, target) { for (let i = 0; i < array.length; i++) { if (!Object.hasOwn(array, i)) continue; const value = array[i]; if (value === target || (Number.isNaN(value) && Number.isNaN(target))) { return i; } } return -1;}
const mixed = [, undefined, NaN];assert.equal(findOwnValue(holes, undefined), -1);assert.equal(findOwnValue(mixed, undefined), 1);assert.equal(findOwnValue(mixed, NaN), 2);assert.equal(findOwnValue(mixed, null), -1);console.log('own entries: undefined=1, NaN=2, null=-1');
assert.equal([0].indexOf(-0), 0);assert.equal([0].includes(-0), true);assert.equal([1].includes('1'), false);const item = { id: 1 };assert.equal([item].includes(item), true);assert.equal([item].includes({ id: 1 }), false);assert.equal(values.includes(NaN, 2), false);assert.equal(values.indexOf(10), 0);assert.equal(Boolean(values.indexOf(10)), false);assert.equal(Boolean(values.indexOf(99)), true);assert.equal([].includes(undefined), false);console.log('identity, type, start index, and Boolean traps passed');Expected output:
NaN: indexOf=-1, includes=true, findIndex=1undefined: hole differs, explicit entry matches bothown entries: undefined=1, NaN=2, null=-1identity, type, start index, and Boolean traps passedThe fixture checks property presence separately from the retrieved value. A hole and an explicit undefined entry both read as undefined here, but only the second has an own property. An empty array has no positions to inspect, so it does not match undefined.
Why the two searches disagree
The ECMAScript includes algorithm compares retrieved values with SameValueZero. That comparison matches NaN with NaN. The adjacent indexOf algorithm uses strict equality and checks property existence before comparing a value. Strict equality never matches NaN to itself.
For ordinary arrays in this fixture, includes reads a missing position as undefined, while indexOf skips it. Neither method treats 0 and -0 as different values, coerces a string probe to a number, or compares separate objects by their fields. These are search semantics, not evidence of corruption in the input array.
The second argument also limits the search range. In the fixture, values.includes(NaN, 2) starts after the matching position and returns false. Check the probe and starting position before assuming that a membership method is broken.
Choose the check your data contract needs
| Reader requirement | Check | Boundary verified above |
|---|---|---|
| Boolean membership, including NaN | array.includes(value) | Holes can satisfy an undefined probe |
| Position of NaN | array.findIndex(Number.isNaN) | Returns the first matching numeric NaN position |
| Position of an ordinary strict-equality match | array.indexOf(value) | Zero is a valid index; -1 means absent |
| Position among own entries, including NaN | findOwnValue(array, value) | Skips absent own properties |
| Object membership | array.includes(existingObject) | Requires the same object reference |
findOwnValue is intentionally a small loop for an explicit policy: only own indexed entries count, with a NaN-aware equality check. It is not a replacement implementation of the built-in methods. Built-ins can observe inherited indexed properties; this helper excludes them. The assertions exercise plain arrays only and do not establish behavior for getters or mutation during the loop.
Do not write if (array.indexOf(value)) to test membership. A match at index zero converts to false, while the absent sentinel -1 converts to true. Compare the index explicitly, or use includes after accepting its NaN and sparse-slot behavior.
For records, changing the membership method will not make a newly constructed { id: 1 } match an existing object. Use a field predicate when ID equality is the intended rule; the separate deduplication by ID article covers choosing which record to retain.
Before changing a shared search helper, run the fixture against its intended contract: NaN, an explicit undefined entry, a hole, index zero, a missing value, and a separate object with matching fields. Choose whether missing positions should count before standardizing on a Boolean membership check.
References: