How to check if an array contains a value in JavaScript
To check if a JavaScript array contains a value, includes() finds NaN where indexOf() cannot. On 100,000 items a Set measured 1,245 times faster.

The short answer:
const colors = ["red", "green", "blue"];
colors.includes("red"); // true
includes() returns a boolean and reads like a sentence. The older form still works and is what you will see in existing code:
colors.indexOf("red") !== -1; // true
They differ in two places worth knowing: includes finds NaN and indexOf cannot, and neither works the way you expect on objects. Everything below was measured in Node v24.14.1.
includes versus indexOf
[NaN].includes(NaN); // true
[NaN].indexOf(NaN); // -1
NaN === NaN; // false
indexOf compares with strict equality, and NaN is famously not equal to itself, so it can never find one. includes uses SameValueZero, which treats two NaN values as the same.
The same difference shows up on sparse arrays, the ones with holes:
[, ,].includes(undefined); // true
[, ,].indexOf(undefined); // -1
includes treats a hole as undefined; indexOf skips holes entirely.
Neither difference comes up often. When it does, indexOf fails silently rather than erroring, which is the worst way for a difference to matter. Default to includes unless you actually need the position, in which case indexOf is the right call.
Both fail on objects
[{ id: 1 }].includes({ id: 1 }); // false
Two objects with identical contents are different values, because objects compare by reference. includes is asking "is this exact object in the array", and the answer is no.
When you want to search by content, use a predicate:
const users = [{ id: 1, name: "Ada" }];
users.some((u) => u.id === 1); // true
users.find((u) => u.id === 1); // { id: 1, name: "Ada" }
users.findIndex((u) => u.id === 1); // 0
Three methods for three questions:
somereturns a boolean. Use it when you only need to know whether something matched.findreturns the matching item, orundefined.findIndexreturns the position, or-1.
Note the two different "not found" values. find gives undefined and findIndex gives -1, which is why if (users.find(...)) and if (users.findIndex(...) !== -1) are both correct and if (users.findIndex(...)) is a bug: index 0 is falsy.
A find that missed is also the most common source of a TypeError about reading a property of undefined, because the natural next step is to read a property from the result.
The performance difference nobody mentions
includes scans the array from the start until it finds a match. For one lookup on a short array, that is irrelevant. Inside a loop over a large array, it is not.
Measured, 1,000 lookups of the last item in a 100,000-element array:
| Approach | Time |
|---|---|
array.includes(x) |
87.2ms |
set.has(x) |
0.07ms |
A Set is roughly 1,245 times faster here, because it hashes rather than scans. The gap widens with array length, since includes grows linearly and Set.has does not.
The pattern that matters is filtering one list against another:
// scans allowed for every item in items
items.filter((i) => allowed.includes(i.id));
// hashes once, then constant-time lookups
const allowedIds = new Set(allowed);
items.filter((i) => allowedIds.has(i.id));
Two lines instead of one, and the cost stops being the product of the two list lengths. The underlying reason is covered in Big O notation explained: one approach grows with the product of the two list lengths, the other with their sum.
For a single check on a list of ten things, includes is clearer and you should use it. The switch is worth making when the lookup is inside a loop.
Searching from a position
includes takes a second argument, the index to start from:
["red", "green", "blue"].includes("red", 1); // false
"red" is at index 0, and the search started at 1, so it was not found. A negative value counts from the end.
indexOf takes the same argument, and lastIndexOf searches backwards, which is how you find the final occurrence of a repeated value.
Checking for several values at once
const wanted = ["red", "blue"];
wanted.some((w) => colors.includes(w)); // any of them
wanted.every((w) => colors.includes(w)); // all of them
some and every compose with includes to answer both questions. For larger lists, the same Set swap applies: build a Set from colors once and call .has inside the callback.
Strings have includes too
"hello world".includes("world"); // true
Same method name, same boolean result, on strings instead of arrays. It replaced the older indexOf(...) !== -1 idiom there as well, and startsWith and endsWith cover the anchored versions.
Worth knowing that the array and string methods are unrelated implementations that happen to share a name and a shape. The array one compares elements; the string one looks for a substring.
Which to use
| Question | Use |
|---|---|
| Is this exact value present? | includes |
| Where is it? | indexOf |
| Is there an item matching a condition? | some |
| Which item matches? | find |
| Which position matches? | findIndex |
| Repeated lookups on a large list | new Set(...) then .has |
Searching nested arrays
includes compares elements, and an element that is itself an array is compared by reference like any other object:
[[1, 2]].includes([1, 2]); // false
Two ways through it, depending on what you mean.
Flatten first, if you want to know whether a value appears anywhere at any depth:
[1, [2, [3]]].flat(Infinity).includes(3); // true
Compare contents, if you want to know whether an equivalent array is present:
arrays.some((a) => a.length === target.length && a.every((v, i) => v === target[i]));
That is a shallow comparison, which is usually enough. For deeper structures, comparing JSON.stringify output works when the values are JSON-safe and the key order is consistent, and both of those are real conditions rather than safe assumptions.
The newer array methods
Three additions worth knowing, because they replace awkward older idioms.
arr.at(-1); // the last item
arr.findLast(fn); // the last match
arr.findLastIndex(fn);
at() takes negative indices, so arr.at(-1) replaces arr[arr.length - 1]. It reads better and cannot be got wrong by an off-by-one.
findLast searches from the end, which used to mean reversing a copy of the array first. Reversing in place to search would have mutated the caller's data, so this is a genuine improvement rather than a shorthand.
arr.includes(x); // is it there
arr.indexOf(x); // where is the first one
arr.lastIndexOf(x); // where is the last one
arr.at(-1); // what is the last item
Checking membership across two lists
The common real task is not "is this one value present" but "which of these values are present":
const wantedSet = new Set(wanted);
const present = items.filter((i) => wantedSet.has(i));
const missing = wanted.filter((w) => !items.includes(w));
Note which side gets the Set. You build it from the list you will check against, repeatedly, and iterate the other one. Building it from the wrong side gains nothing.
Current runtimes also have set operations directly:
new Set(a).intersection(new Set(b)); // in both
new Set(a).difference(new Set(b)); // in a only
Those read better than the filter versions and do the same work. Where support matters, the filter plus Set.has form works everywhere and is equally fast.
Case-insensitive membership
includes compares exactly, so casing and whitespace both matter:
["Red"].includes("red"); // false
Normalise both sides rather than the array alone, since the value being searched for usually comes from user input too:
const norm = (s) => s.trim().toLowerCase();
colors.some((c) => norm(c) === norm(value));
For repeated lookups, normalise once into a Set:
const set = new Set(colors.map(norm));
set.has(norm(value));
That is the same Set swap as before, with the normalisation folded into the construction so it happens once per item rather than once per comparison. It is also where the accent-normalisation point from string handling applies: two visually identical strings can be different sequences, and normalize("NFC") on both sides is what makes them match.
Three common mistakes
Using indexOf to search for NaN. It always returns -1. includes finds it.
Expecting includes to match objects by content. It compares references, so a freshly built object never matches. Use some with a predicate.
Testing findIndex for truthiness. A match at position 0 is falsy, so if (arr.findIndex(...)) silently misses the first element. Compare against -1.
Counting and filtering, not just checking
"Does it contain" is often the first version of a question that turns out to be about counts or subsets:
arr.filter((x) => x === value).length; // how many times
arr.every((x) => allowed.includes(x)); // are they all allowed
!arr.some((x) => banned.includes(x)); // is none of them banned
every on an empty array returns true, which is correct by the mathematical definition and occasionally not what a caller expects. An empty basket passing "are all items in stock" is technically right and worth a deliberate check when it matters.
For counting several values at once, one pass beats repeated filtering:
const counts = new Map();
for (const x of arr) counts.set(x, (counts.get(x) ?? 0) + 1);
That builds every count in a single traversal, where filter().length per value walks the array once for each value you ask about.
What "contains" means for your data
The methods on this page all answer a question you have already decided. The decision is what equality means, and it is worth making explicitly:
- Identity. Is this exact object in the list?
includes. - A field matches. Is there an item with this id?
somewith a predicate. - Contents match. Is there an item equal to this one field by field? A comparison function, or a key derived from the fields.
Most bugs in this area come from using the first when you meant the second, because the first is shorter and appears to work on the primitives you tested with. Objects arriving from an API are always new objects, so reference equality against them is reliably false.
Quick reference
arr.includes(x) // boolean, finds NaN
arr.includes(x, from) // start from an index
arr.indexOf(x) !== -1 // older form, cannot find NaN
arr.some(fn) // any item matches a condition
arr.find(fn) // the item, or undefined
arr.findIndex(fn) // the index, or -1
new Set(arr).has(x) // for repeated lookups
Want to time these against a large array yourself? Start with the JavaScript track.
More from the blog

How to merge two arrays in JavaScript
To merge arrays in JavaScript, spread and concat both work. push(...arr) throws a RangeError at 200,000 items, which is why the choice matters.
Read more
How to loop through an object in JavaScript
To loop through an object in JavaScript, for...in walks inherited properties too. And integer-like keys come out first, whatever order you wrote.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.