JavaScript (ES2026) Module 6 – Arrays and array methods
Searching: includes, indexOf, find, some and every
Search JavaScript arrays by value with includes and indexOf, by condition with find, some and every, from the end with findLast, and know how NaN is handled.
What you will learn
- Pick the right search method for value vs condition searches
- Search from the end with findLast and findLastIndex
- Explain NaN handling differences between search methods
Before you start
What you’ll learn · Before you start
What you will learn
- Pick the right search method for value vs condition searches
- Search from the end with findLast and findLastIndex
- Explain NaN handling differences between search methods
Before you start
On this page
JavaScript arrays have two families of search methods. To look for a value, includes(value) answers true or
false, and indexOf(value) gives the index of the first match, or -1. To look for an item that meets a
condition, you pass a function that returns true or false for an item: find(fn) returns the first item that
passes, or undefined; findIndex(fn) returns that item’s index, or -1; some(fn) asks whether any item passes and
every(fn) whether all of them do. findLast() and findLastIndex() search from the end. Every one of them stops as
soon as it knows the answer. This lesson runs each method, shows where they disagree (NaN, objects and empty
arrays) and measures when an array is the wrong place to search at all.
Searching for a value: includes and indexOf
// Searching for a known value: is it there (includes), and at which index (indexOf, lastIndexOf)?
const waitlist = [104, 87, 23, 56, 23];
console.log('includes(23):', waitlist.includes(23), '| includes(99):', waitlist.includes(99));
console.log('indexOf(23):', waitlist.indexOf(23), '| lastIndexOf(23):', waitlist.lastIndexOf(23));
console.log('indexOf(99):', waitlist.indexOf(99));
console.log('indexOf(23, 3):', waitlist.indexOf(23, 3), '| includes(104, 1):', waitlist.includes(104, 1));
console.log("strict: includes('23'):", waitlist.includes('23'), "| indexOf('23'):", waitlist.indexOf('23'));
// The classic trap: index 0 is falsy, and -1 is truthy.
console.log('if (indexOf(104)) would run:', Boolean(waitlist.indexOf(104)));
console.log('if (indexOf(99)) would run:', Boolean(waitlist.indexOf(99)));
console.log('the right test:', waitlist.indexOf(104) !== -1, waitlist.includes(104)); Output
includes(23): true | includes(99): false
indexOf(23): 2 | lastIndexOf(23): 4
indexOf(99): -1
indexOf(23, 3): 4 | includes(104, 1): false
strict: includes('23'): false | indexOf('23'): -1
if (indexOf(104)) would run: false
if (indexOf(99)) would run: true
the right test: true true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node value-search.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
includes(23) only says whether 23 is there. indexOf(23) says where: index 2, the first of the two matches, and
lastIndexOf(23) searches from the end and finds the other one at index 4. When the value is missing, indexOf() and
lastIndexOf() return -1, never undefined. A second argument tells the search where to start, so
indexOf(23, 3) skips the first match and includes(104, 1) misses the 104 at index 0. Both methods compare without
converting types: the string '23' is not the number 23, so neither of them finds it.
The last three lines show the classic mistake with indexOf(). Its result is a position, and position 0 is a falsy
number, so if (waitlist.indexOf(104)) skips its block although 104 is right at the front, while -1 is truthy, so
if (waitlist.indexOf(99)) runs its block for a number that is not there. When you only need yes or no, use
includes(); when you need the position too, compare it with -1.
NaN, -0 and objects
The two value searches compare in different ways, and the difference shows on three kinds of value:
// includes() compares with SameValueZero and indexOf() with ===, so they disagree about NaN. Both compare objects by
// reference: a look-alike object is a different object.
const readings = [21.5, NaN, 23.5, -0];
console.log('includes(NaN):', readings.includes(NaN), '| indexOf(NaN):', readings.indexOf(NaN));
console.log('findIndex(Number.isNaN):', readings.findIndex(Number.isNaN));
console.log('includes(0):', readings.includes(0), '| indexOf(0):', readings.indexOf(0));
const asha = { id: 7, name: 'Asha' };
const users = [asha, { id: 9, name: 'Ravi' }];
console.log('the same object:', users.includes(asha), '| a look-alike:', users.includes({ id: 7, name: 'Asha' }));
console.log('by a condition:', users.some((u) => u.id === 7), users.findIndex((u) => u.id === 9));
const withHole = [1, , 3];
console.log('a hole: includes(undefined)', withHole.includes(undefined), '| indexOf(undefined)', withHole.indexOf(undefined)); Output
includes(NaN): true | indexOf(NaN): -1 findIndex(Number.isNaN): 1 includes(0): true | indexOf(0): 3 the same object: true | a look-alike: false by a condition: true 1 a hole: includes(undefined) true | indexOf(undefined) -1
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node nan-search.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
includes() uses the comparison the specification calls SameValueZero: it is ===, except that NaN counts as
equal to NaN. indexOf() and lastIndexOf() use plain ===, and NaN === NaN is false, so they can never find
a NaN, a common value in sensor readings or in numbers parsed from bad input. To find its position, search by a
condition with findIndex(Number.isNaN). Both comparisons treat 0 and -0 as equal.
Objects are compared by reference, as === compares them: users.includes(asha) finds the very object stored in the
array, but a new object with the same properties is a different object, so the look-alike is not found. To find a
user by their id, search with a condition: some() answers yes or no, and find() or findIndex() locate the
match. The last line shows one more difference that the specification notes: includes() reads a hole as undefined,
while indexOf() skips holes, so they also disagree about undefined in a sparse array.
Searching by a condition: find and findIndex
find() calls your function with each item in turn, from index 0, and returns the first item for which it returns a
truthy value. findIndex() does the same and returns the index instead:
// Departures from one platform, in time order. Times are 'HH:MM' strings with leading zeros, so comparing them as
// strings compares them as times.
const departures = [
{ train: 'Intercity', at: '06:10', seats: 0 },
{ train: 'Passenger', at: '09:45', seats: 120 },
{ train: 'Express', at: '13:30', seats: 0 },
{ train: 'Superfast', at: '18:05', seats: 14 },
{ train: 'Mail', at: '21:40', seats: 0 },
{ train: 'Night Express', at: '23:55', seats: 3 },
];
const leavesAfter = (time) => (d) => d.at > time;
console.log('first after 10:00:', departures.find(leavesAfter('10:00')));
console.log('its index:', departures.findIndex(leavesAfter('10:00')));
console.log('first after 10:00 with seats:', departures.find((d) => d.at > '10:00' && d.seats > 0).train);
console.log('last of the day with seats:', departures.findLast((d) => d.seats > 0).train);
console.log('index of the last full train:', departures.findLastIndex((d) => d.seats === 0));
console.log('first after 23:59:', departures.find(leavesAfter('23:59')), departures.findIndex(leavesAfter('23:59'))); Output
first after 10:00: { train: 'Express', at: '13:30', seats: 0 }
its index: 2
first after 10:00 with seats: Superfast
last of the day with seats: Night Express
index of the last full train: 4
first after 23:59: undefined -1
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node train-search.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The first departure after 10:00 is the Express at index 2, although it has no seats; adding d.seats > 0 to the
condition skips it and finds the Superfast. leavesAfter('10:00') is a function that returns the test function, a
pattern from the functions module that keeps conditions readable. The times work as strings only because every one
has two digits for the hour and two for the minutes, so '09:45' < '13:30' compares them character by character in
time order.
find() returns the item itself, not a copy: changing the object it returns changes the object in the array. When
nothing passes, it returns undefined and findIndex() returns -1. If the array can hold undefined values
themselves, find()’s answer is ambiguous, so use findIndex() and compare with -1. The function also receives the
index and the whole array as its second and third arguments, as the next lesson’s methods do.
Searching from the end: findLast and findLastIndex
findLast() and findLastIndex(), added in ECMAScript 2023, work like find() and findIndex() but start at the
last index and move towards the front. In the timetable, findLast((d) => d.seats > 0) gives the last train of the
day that still has seats, the Night Express, and findLastIndex((d) => d.seats === 0) gives 4, the index of the
Mail, the last full train. Use them whenever “the most recent” or “the last” is the question: the latest payment that
failed, the last attempt that passed. The older way, [...list].reverse().find(fn), copies the whole array first, and
its findIndex() version counts positions in the reversed copy.
Version note
findLast() and findLastIndex() need Node.js 18 or later, Chrome 97, Firefox 104 or Safari 15.4.
Yes or no: some and every
some(fn) returns true as soon as one item passes, and every(fn) returns false as soon as one item fails. Both
stop there, which the call counts show:
// some() stops at the first true answer and every() at the first false one. On an empty array, some() is false and
// every() is true.
const marks = [72, 35, 91, 48];
let calls = 0;
const failed = (mark) => {
calls++;
return mark < 40;
};
console.log('some failed?', marks.some(failed), 'after', calls, 'calls');
calls = 0;
const passed = (mark) => {
calls++;
return mark >= 40;
};
console.log('every passed?', marks.every(passed), 'after', calls, 'calls');
const noMarks = [];
console.log('empty: some failed?', noMarks.some(failed), '| every passed?', noMarks.every(passed));
console.log('"all passed" that needs at least one mark:', noMarks.length > 0 && noMarks.every(passed)); Output
some failed? true after 2 calls every passed? false after 2 calls empty: some failed? false | every passed? true "all passed" that needs at least one mark: false
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node every-empty.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The second mark, 35, is below 40, so some(failed) stopped after 2 calls, and every(passed) stopped at the same
mark. The surprise comes with an empty array: some() returns false, because no item passed, and every() returns
true, because no item failed. The specification says it directly: every acts like the “for all” of mathematics,
and for an empty array it returns true. That is logical, and it is a bug whenever an empty list means that
something went wrong, such as a list of checks that failed to load. Test the length as well, as the last line does,
when “nothing to check” must not count as success.
Which search method answers which question
Text description of the diagram
The diagram starts with one question: what are you searching with?
- With a value, which is compared with === or SameValueZero, two boxes follow. includes(v) answers true or false. indexOf(v) and lastIndexOf(v) give the index of the first or the last match, or -1.
- When there is no exact value to compare, a dashed arrow leads to searches with a condition, a function that returns true or false for an item. find(fn) and findLast(fn) give the first or the last item that passes, or undefined. findIndex(fn) and findLastIndex(fn) give its index, or -1. some(fn) asks whether any item passes, and every(fn) whether all of them do.
| Method | Returns | On an empty array |
|---|---|---|
includes(v) |
true or false |
false |
indexOf(v), lastIndexOf(v) |
the index of the first or last match, or -1 | -1 |
find(fn), findLast(fn) |
the first or last item that passes, or undefined |
undefined |
findIndex(fn), findLastIndex(fn) |
the index of that item, or -1 | -1 |
some(fn) |
true when any item passes |
false |
every(fn) |
true when no item fails |
true |
Many lookups: build a Set
Each of these methods walks the array, item by item, so a value that is not there costs a read of every item. One search is cheap. Thousands of searches against the same long list are not:
// 2,000 sign-ups checked against 50,000 usernames already taken. includes() scans the array on every check; a Set
// is built once and then finds a name without scanning. Times are the fastest of three runs, in milliseconds.
const taken = Array.from({ length: 50_000 }, (_, i) => `user${i}`);
const signUps = Array.from({ length: 2_000 }, (_, i) => `user${i * 37}`);
function countTaken(isTaken) {
let count = 0;
for (const name of signUps) if (isTaken(name)) count++;
return count;
}
function fastest(task) {
let best = Infinity;
let result;
for (let run = 0; run < 3; run++) {
const start = performance.now();
result = task();
best = Math.min(best, performance.now() - start);
}
return [result, best];
}
const [clashes, scanMs] = fastest(() => countTaken((name) => taken.includes(name)));
const [set, buildMs] = fastest(() => new Set(taken));
const [clashes2, lookupMs] = fastest(() => countTaken((name) => set.has(name)));
console.log(`names already taken: ${clashes} of ${signUps.length} (the Set agrees: ${clashes === clashes2})`);
console.log(`includes() on every sign-up: ${scanMs.toFixed(1).padStart(7)} ms`);
console.log(`new Set(taken), once: ${buildMs.toFixed(1).padStart(7)} ms`);
console.log(`set.has() on every sign-up: ${lookupMs.toFixed(2).padStart(7)} ms`); Output
names already taken: 1352 of 2000 (the Set agrees: true) includes() on every sign-up: 346.0 ms new Set(taken), once: 3.3 ms set.has() on every sign-up: 0.29 ms
This output changes from run to run: the times depend on the computer and on what else it is running
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node set-lookups.mjs
Of the 2,000 sign-ups, 648 picked a name nobody had, and each of those checks read all 50,000 taken names, more than
32 million string comparisons for those alone. A Set stores each value once and finds a value without scanning:
the specification requires access times that, on average, grow more slowly than the number of items. Building it is a
single pass over the array, and after that every check is a lookup. In the run above, the 2,000 checks took hundreds
of times less time with set.has() than with includes(), and the Set still wins by far once the time to build it
is added. A Set compares with SameValueZero, like includes(). So: one search, use an array method; many searches
against the same list, build a Set once, or a Map from each key to its item, which the collections module covers.
Exercises
Exercise · Easy · JavaScript
Find the latest passing attempt and check that every invoice is paid
Write two functions in checks.mjs.
latestPassingAttempt(attempts, passMark)gets a learner's attempts at a test in the order they were made, each an object such as{ attempt: 3, score: 64 }, and returns the most recent attempt whose score is at leastpassMark, orundefinedwhen none is. A score that isNaN(the marking failed) never counts as passing.allPaid(invoices)gets invoices such as{ id: 'INV-7', status: 'paid' }and returnstruewhen there is at least one invoice and every invoice has the status'paid'. A customer with no invoices at all getsfalse: there is nothing to show as paid.
The starter returns the first passing attempt instead of the latest one, and its allPaid() says true for an empty list. Run the sample tests when you are done.
Starter code · checks.mjs
// The checks of lesson javascript/searching-arrays.
export function latestPassingAttempt(attempts, passMark) {
// Returns the first passing attempt, not the latest one.
return attempts.find((a) => a.score >= passMark);
}
export function allPaid(invoices) {
// Says true for a customer with no invoices.
return invoices.every((invoice) => invoice.status === 'paid');
} The sample tests · checks.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { latestPassingAttempt, allPaid } from './checks.mjs';
const attempts = [
{ attempt: 1, score: 55 },
{ attempt: 2, score: 72 },
{ attempt: 3, score: 64 },
{ attempt: 4, score: 38 },
];
test('the latest attempt at or above the mark', () => {
assert.deepEqual(latestPassingAttempt(attempts, 60), { attempt: 3, score: 64 });
assert.deepEqual(latestPassingAttempt(attempts, 72), { attempt: 2, score: 72 });
});
test('undefined when no attempt passes', () => {
assert.equal(latestPassingAttempt(attempts, 80), undefined);
assert.equal(latestPassingAttempt([], 40), undefined);
});
test('a NaN score never passes', () => {
const marked = [{ attempt: 1, score: 70 }, { attempt: 2, score: NaN }];
assert.deepEqual(latestPassingAttempt(marked, 50), { attempt: 1, score: 70 });
});
test('every invoice paid', () => {
assert.equal(allPaid([{ id: 'INV-1', status: 'paid' }, { id: 'INV-2', status: 'paid' }]), true);
});
test('one invoice still due', () => {
assert.equal(allPaid([{ id: 'INV-1', status: 'paid' }, { id: 'INV-2', status: 'due' }]), false);
});
test('no invoices at all is not "all paid"', () => {
assert.equal(allPaid([]), false);
}); A hint
find() searches from the front and stops at the first match; another method searches from the back. A comparison with NaN, such as NaN >= 50, is always false, so a plain >= test already rejects those scores. For allPaid(), remember what every() returns for an empty array, and check the length first.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
7 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the difference between includes() and indexOf() on a JavaScript array?
includes(value) returns true or false, while indexOf(value) returns the index of the first match, or -1 when
there is none. They also compare differently. includes() uses SameValueZero, so it finds NaN; indexOf() uses
===, so [NaN].indexOf(NaN) is -1. Both treat 0 and -0 as equal, compare objects by reference and never convert
types, so [1].includes('1') is false. includes() reads holes as undefined, while indexOf() skips them. Use
includes() for yes-or-no checks, because if (list.indexOf(x)) is wrong for a match at index 0, and use indexOf()
when you need the position. For many lookups against a large array, build a Set once.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A release script runs const ok = checks.every((c) => c.passed) before deploying. One night the CI configuration failed to load, checks came back as an empty array, and a broken build went to production. Explain why the script said yes, and how you would make it fail closed.
-
Mid to senior
A sign-up endpoint checks each new username against 300,000 reserved and taken names with reserved.includes(name.toLowerCase()). It is fast in tests but slow under load. Estimate the cost, propose a better structure with numbers, and say what changes about how names are compared.
Key takeaways
- With a value:
includes()answers yes or no,indexOf()andlastIndexOf()give the first or last index, or -1. Never testindexOf()for truthiness: compare it with -1. - With a condition:
find()andfindLast()give the item orundefined;findIndex()andfindLastIndex()give its index or -1. includes()findsNaNandindexOf()cannot; usefindIndex(Number.isNaN)for its position. Objects are found only by reference, so search for them with a condition.some()stops at the first item that passes andevery()at the first that fails. On an empty arraysome()isfalseandevery()istrue.- Every array search reads the items one by one. For many lookups against the same list, build a
Setonce.
References
- ECMAScript 2026 Language Specification: Array.prototype.includes (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.indexOf (Ecma International)
- ECMAScript 2026 Language Specification: SameValueZero (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.find (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.findLast (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.every (Ecma International)
- ECMAScript 2026 Language Specification: Set Objects (Ecma International)
- Array.prototype.includes() (MDN Web Docs)
- Array.prototype.findLast() (MDN Web Docs)
- Array.prototype.some() (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress