700 words
4 minutes

JavaScript indexOf Cannot Find NaN: Check includes and Sparse Slots

2026-10-24
Frontend
JavaScript
/
Troubleshooting
/
Node.js
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=1
undefined: hole differs, explicit entry matches both
own entries: undefined=1, NaN=2, null=-1
identity, type, start index, and Boolean traps passed

The 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 requirementCheckBoundary verified above
Boolean membership, including NaNarray.includes(value)Holes can satisfy an undefined probe
Position of NaNarray.findIndex(Number.isNaN)Returns the first matching numeric NaN position
Position of an ordinary strict-equality matcharray.indexOf(value)Zero is a valid index; -1 means absent
Position among own entries, including NaNfindOwnValue(array, value)Skips absent own properties
Object membershiparray.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:

JavaScript indexOf Cannot Find NaN: Check includes and Sparse Slots
https://laplusda.com/en/posts/javascript-indexof-nan-includes-sparse-arrays/
Author
Zero
Published at
2026-10-24
License
CC BY-NC-SA 4.0