Finding and ordering data
Two of the most common things you do with arrays are find an element and put elements in order. JavaScript provides several searching methods with subtly different behavior, and a sorting method with one of the language's most infamous gotchas. Getting these right — and knowing the traps — is everyday-essential and a frequent interview topic. This guide covers how to search effectively and how to sort correctly, including objects and multi-key cases.
Searching by value: indexOf, lastIndexOf, includes
indexOf returns the index of the first strictly-equal (===) element, or -1 if
absent. lastIndexOf does the same from the end. includes returns a boolean.
const a = ['x', 'y', 'z', 'y']
a.indexOf('y') // 1
a.lastIndexOf('y') // 3
a.includes('y') // true when you just need yes/no
a.indexOf('q') // -1 (not found)
A classic bug is treating the -1 result as falsy: if (a.indexOf(x)) is wrong because
index 0 is also falsy. Compare with !== -1, or use includes when you only need a
boolean.
The NaN gotcha
indexOf uses ===, and NaN === NaN is false — so indexOf can never find NaN.
includes uses the SameValueZero algorithm, which treats NaN as equal to itself.
[NaN].indexOf(NaN) // -1 can't find it
[NaN].includes(NaN) // true
If you need the index of a NaN, use findIndex(Number.isNaN). This difference is a
favorite interview question because it reveals whether you know how equality works under each
method.
Searching by predicate: find, findIndex
When you're searching for "the element where some condition holds" (especially objects),
find and findIndex take a predicate function rather than a value.
const users = [{ id: 1 }, { id: 7 }]
users.find(u => u.id === 7) // { id: 7 } — the object
users.findIndex(u => u.id === 7) // 1 — its index
users.includes({ id: 7 }) // false different reference
Note includes/indexOf compare by identity, so they can't find an object "that looks the
same." Use find/some with a predicate for value-based object searches. findLast and
findLastIndex search from the end — handy for "most recent" lookups.
The sort() string-coercion gotcha
Here's the big one. Array.prototype.sort with no comparator converts elements to strings
and compares them by UTF-16 code unit. For numbers, this produces nonsense:
[10, 1, 2, 20].sort() // [1, 10, 2, 20] "10" sorts before "2"
[10, 1, 2, 20].sort((a, b) => a - b) // [1, 2, 10, 20]
Always pass a comparator when sorting numbers. This default trips up nearly every developer at least once.
Writing comparators
A comparator returns a negative number if a should come first, positive if b
should, and 0 to keep their order.
arr.sort((a, b) => a - b) // ascending numbers
arr.sort((a, b) => b - a) // descending numbers
The a - b subtraction trick works only for numbers. Don't return a boolean (a > b) — it
coerces to 0/1, never negative, so the sort is broken. For strings, use localeCompare:
words.sort((a, b) => a.localeCompare(b)) // human-friendly string order
sort mutates — beware
sort (and reverse) sort in place and return the same array reference. If the array is
shared, you've just mutated everyone's copy.
const a = [3, 1, 2]
const sorted = a.sort((x, y) => x - y)
sorted === a // true original mutated
const safe = a.toSorted((x, y) => x - y) // ES2023 new array, a untouched
const safe2 = [...a].sort((x, y) => x - y) // copy-then-sort
In immutable contexts (React state), always copy first or use toSorted.
Stable sort and multi-key sorting
Since ES2019, sort is guaranteed stable: elements the comparator treats as equal keep
their original relative order. This makes multi-key sorting reliable — sort by the
lowest-priority key first, then the next, and so on.
people.sort((a, b) => a.firstName.localeCompare(b.firstName)) // tiebreaker
.sort((a, b) => a.age - b.age) // primary key
// stability keeps name order within each age group
A cleaner single-pass approach uses the || fall-through trick: return the first non-zero
comparison.
people.sort((a, b) =>
a.lastName.localeCompare(b.lastName) || // primary
a.firstName.localeCompare(b.firstName) || // tiebreaker
a.age - b.age // final tiebreaker
)
Locale-aware and natural sorting
localeCompare accepts options for case-insensitive and "natural" numeric sorting (so
file2 comes before file10):
['file10', 'file2'].sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true })) // ['file2', 'file10']
words.sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: 'base' })) // case/accent-insensitive
For large arrays, build a reusable Intl.Collator and pass its compare method — it's much
faster than calling localeCompare per pair.
When to reach for a Set or Map
Repeated includes/indexOf lookups are O(n) each. If you search the same collection
many times, build a Set once for O(1) membership tests.
const seen = new Set(bigArray)
queries.filter(q => seen.has(q)) // O(1) per lookup
Likewise, if you constantly look elements up by a key, index them in a Map rather than
scanning with find every time. Choosing the right data structure beats optimizing the
search.
Key takeaways
indexOf/lastIndexOffind by===(return-1when absent);includesreturns a boolean and, unlikeindexOf, can findNaN.- Use
find/findIndexfor predicate/object searches;includescompares objects by identity. sort()with no comparator stringifies elements — always pass a comparator for numbers.- Comparators return negative/positive/zero;
a - bfor numbers,localeComparefor strings. sort/reversemutate in place; usetoSorted/toReversedor copy first for immutability.- Sort is stable (ES2019+), enabling multi-key sorting; use
Intloptions for case- insensitive and natural ordering, and aSet/Mapfor repeated lookups.
Search with the method that matches your equality needs, and sort with a deliberate comparator — and the array's most notorious gotcha stops being a problem.