JAVASCRIPT / ARRAYS
Searching with includes, indexOf, and find
Choose between includes, indexOf, and find to test membership, locate a position, or pull out the first element matching a condition, and handle misses.
What you will learn
- Pick includes for a yes/no answer and indexOf when you need the position
- Test indexOf and findIndex against -1 rather than for truthiness
- Match objects by property with find, since equality compares references
- Remember includes finds NaN and indexOf never does
Understanding Searching with includes, indexOf, and find
Three different questions come up when you search an array, and each method answers exactly one of them. includes answers whether a value is present and gives back a boolean. indexOf answers where the value sits and gives back an index, or -1 when there is nothing to report. find answers which element satisfies a condition: you hand it a function, it runs that function on each element in order, and it returns the first element for which the function returns true.
The first two compare values using fixed built-in rules, and those rules have consequences. indexOf uses the same comparison as ===, which is why it can never locate NaN: NaN === NaN is false, so the scan runs off the end and returns -1. includes uses SameValueZero, which is === with a single amendment, namely that NaN counts as equal to itself. Neither method looks inside objects, so an object literal written at the call site is a brand new reference that matches nothing in the array; as soon as a match depends on a property rather than on the whole value, find is the right tool.
The shape of each return value dictates how you branch on it. -1 is an ordinary number and index 0 is falsy, so indexOf and findIndex results must be compared against -1 instead of tested for truth, and includes exists so membership checks can read as plain booleans. find signals a miss with undefined, which is ambiguous when undefined is a legitimate element, and findIndex sidesteps that ambiguity. All of them walk the array from the front and stop at the first hit, so cost grows with length; lastIndexOf and findLast walk from the other end, and a Set built once beats repeated includes calls against a long fixed list.
const stock = ['pen', 'ink', 'pad', 'ink'];
console.log(stock.includes('ink'));
console.log(stock.indexOf('ink'));
console.log(stock.lastIndexOf('ink'));
console.log(stock.indexOf('glue'));
const orders = [
{ id: 7, qty: 2 },
{ id: 12, qty: 5 },
{ id: 19, qty: 1 }
];
console.log(orders.includes({ id: 12, qty: 5 }));
console.log(orders.find(o => o.id === 12));
console.log(orders.findIndex(o => o.id === 12));
console.log(orders.find(o => o.qty > 99));includes and indexOf search for a value under fixed equality rules, while find delegates the test to a predicate you write and returns the element itself.
Worked examples
NaN and negative zero
Shows the one comparison rule where includes and indexOf disagree.
const readings = [12.5, NaN, 0.5];
console.log(readings.indexOf(NaN));
console.log(readings.includes(NaN));
console.log(readings.findIndex(Number.isNaN));
console.log([0, -0].indexOf(-0));Example explained
Line 1indexOf compares with strict equality, and NaN === NaN is false, so the scan matches nothing and returns the -1 sentinel.
Line 2includes compares with SameValueZero, which differs from === in exactly one case: NaN is treated as equal to itself.
Line 3findIndex calls Number.isNaN on each element, so a predicate can locate the position that indexOf cannot.
Line 4Both methods treat -0 and 0 as the same value, so searching for -0 matches the 0 stored at index 0.
Why indexOf must be compared to -1
Demonstrates the falsy-zero trap and the optional starting index.
const queue = ['ready', 'busy', 'ready'];
if (queue.indexOf('ready')) {
console.log('branch A');
} else {
console.log('branch B: index 0 is falsy');
}
console.log(queue.indexOf('ready') !== -1);
console.log(queue.indexOf('ready', 1));
console.log(queue.includes('busy', 2));
console.log(queue.indexOf('ready', -1));Example explained
Line 1The match sits at index 0, and 0 is falsy, so a successful search takes the else branch.
Line 2Had 'ready' been absent, indexOf would return -1, which is truthy, so the same test would report a miss as a hit.
Line 3Comparing to -1 is the only correct reading of the number, and includes lets you drop the comparison entirely.
Line 4The second argument sets the starting index; a negative one counts back from the end, so -1 begins the scan at the last slot.
find stops early, filter does not
Counts predicate calls to show that find short-circuits and returns an element rather than an array.
const nums = [4, 9, 16, 25];
let seen = 0;
const first = nums.find(n => { seen++; return n > 8; });
console.log(first, seen);
let checked = 0;
const all = nums.filter(n => { checked++; return n > 8; });
console.log(all, checked);Example explained
Line 1find returns as soon as the predicate is true, so the counter reaches 2 of the 4 elements.
Line 2The value handed back is the element 9 itself, not its index and not a one-element array.
Line 3filter has no reason to stop, so it inspects all four elements and allocates a new array.
Line 4Writing filter(...)[0] to get a single match therefore costs a full scan plus an array you immediately discard.
Important notes
Array includes matches whole elements, unlike String includes which matches substrings: ['ab', 'cd'].includes('a') is false.
includes reports holes as undefined while indexOf skips them, so [, ,].includes(undefined) is true but [, ,].indexOf(undefined) is -1.
Common mistakes
Writing if (list.indexOf(item)) — a hit at index 0 falls into the else branch, and a miss returns -1, which is truthy, so both outcomes are reported backwards.
Calling users.includes({ id: 3 }) on an array of objects — the literal is a fresh reference, so the result is false regardless of the contents; find(u => u.id === 3) is the fix.
Reading a property directly off find's result — when nothing matches the result is undefined and the next property access throws TypeError: Cannot read properties of undefined.
Try it yourself
Change, predict, then run
In a browser console, build const carts = [{ user: 'ada', items: 3 }, { user: 'lin', items: 0 }] and log carts.includes({ user: 'lin', items: 0 }), carts.findIndex(c => c.user === 'lin'), and carts.find(c => c.items > 5). Add a comment explaining why the first result is false and the third is undefined.
Open the JavaScript workspaceCheck your understanding
An array holds { id: 4, name: 'bolt' }. Why does products.includes({ id: 4, name: 'bolt' }) return false?
- includes only supports arrays of primitives and throws when the array holds objects
- The object literal in the call is a new reference, and includes compares references rather than contents
- includes compares only the first property of each object, and the order of properties differs
- includes needs a second argument naming the property it should compare
Show answer
For objects, SameValueZero is identity: the literal written inside the call creates a distinct object, so it is not the same value as any element even when the properties match, which is why property-based searches belong to find. Option 0 is tempting because the failure feels like a type restriction, but includes accepts objects without error; it simply answers a question about identity rather than structure.