Cheatsheet

Every array method, what it returns, what it mutates

One page, every method on Array.prototype plus the static helpers, grouped by what you are trying to do. Each row gives the return value, a plain yes or no on mutation, and a one-line example you can paste into a console. The mutation column is the point: almost every array bug is a method that changed the original when you thought it made a copy.

The mutation rule, and the immutable four

Nine methods mutate the array they are called on: push, pop, shift, unshift, splice, sort, reverse, fill, and copyWithin. Everything else returns something new. Memorize that list and the rest of this page is reference rather than trivia.

Since ES2023 there are non-mutating twins for the awkward ones, and they are the right default in React, Redux, Zustand, or anywhere else state is compared by reference.

const a = [3, 1, 2];

a.sort();          // MUTATES a, returns the same array
a.toSorted();      // [1, 2, 3] - a is untouched
a.toReversed();    // [2, 1, 3] - a is untouched
a.toSpliced(1, 1); // [3, 2]    - a is untouched
a.with(0, 99);     // [99, 1, 2] - a is untouched

// The old workaround, still fine and still common:
const sorted = [...a].sort();

Gotcha: the four immutable methods are copies, not deep clones. Objects inside the new array are still the same objects. For a real deep copy use structuredClone(arr).

Create and convert arrays

Method Returns Mutates Example
Array.of() New array of the arguments No Array.of(7) // [7]
Array.from() New array from any iterable or array-like No Array.from("ab") // ["a","b"]
Array.from(obj, fn) Mapped array, without an intermediate copy No Array.from({length:3}, (_, i) => i) // [0,1,2]
Array.fromAsync() Promise resolving to an array No await Array.fromAsync(stream)
Array.isArray() Boolean, and works across realms No Array.isArray([]) // true
new Array(n) Holey array of length n, no elements No new Array(3).fill(0) // [0,0,0]
[...iterable] New array, shallow No [...new Set([1,1,2])] // [1,2]
Object.groupBy() Null-prototype object of string keys to arrays No Object.groupBy(users, u => u.role)
Map.groupBy() Map, so keys can be objects No Map.groupBy(items, i => i.owner)
structuredClone() Deep copy, including nested objects and Maps No structuredClone(rows)

Gotcha: new Array(3).map(fn) returns three holes untouched, because map skips empty slots. Array.from({length: 3}, fn) does what you meant.

Add and remove elements

This group is where almost all of the mutation lives.

Method Returns Mutates Example
push() The new length, not the array Yes a.push(4) // 4
pop() The removed last element Yes [1,2].pop() // 2
shift() The removed first element Yes [1,2].shift() // 1
unshift() The new length Yes a.unshift(0) // 4
splice() Array of what was removed Yes a.splice(1, 2, "x") // removed pair
toSpliced() New array with the splice applied No [1,2,3].toSpliced(1,1) // [1,3]
fill() The same array Yes new Array(3).fill(null)
copyWithin() The same array Yes [1,2,3,4].copyWithin(0,2) // [3,4,3,4]
a.length = n n. Truncates or pads with holes Yes a.length = 0 // empties in place
delete a[i] true, and leaves a hole. Avoid it Yes delete a[1] // length unchanged

Gotcha: push returns a number, so const next = arr.push(x) gives you a length, not the array. Chaining after push is almost always a bug.

Find and test

Method Returns Mutates Example
at() Element at an index, negatives count from the end No [1,2,3].at(-1) // 3
indexOf() First index or -1, strict equality No ["a","b"].indexOf("b") // 1
lastIndexOf() Last matching index or -1 No [1,2,1].lastIndexOf(1) // 2
includes() Boolean, and it finds NaN unlike indexOf No [NaN].includes(NaN) // true
find() First matching element or undefined No users.find(u => u.id === 3)
findIndex() Index of the first match or -1 No users.findIndex(u => u.admin)
findLast() Last matching element or undefined No logs.findLast(l => l.level === "error")
findLastIndex() Index of the last match or -1 No logs.findLastIndex(l => l.ok)
some() Boolean, stops at the first true No nums.some(n => n < 0) // any negative
every() Boolean, stops at the first false. True on an empty array No [].every(Boolean) // true

Gotcha: find returning undefined is indistinguishable from finding a real undefined element. When that matters, use findIndex and compare against -1.

Transform

Method Returns Mutates Example
map() New array of the same length No [1,2].map(n => n * 2) // [2,4]
filter() New array of elements that passed No list.filter(Boolean) // drops falsy
reduce() Whatever the accumulator ends up as No nums.reduce((s, n) => s + n, 0)
reduceRight() Same, walking from the end No fns.reduceRight((v, f) => f(v), x)
flat() New array flattened one level by default No [1,[2,[3]]].flat(Infinity) // [1,2,3]
flatMap() map then flat(1), in one pass No rows.flatMap(r => r.tags)
flatMap() as filter-map Return [] to drop, [x] to keep No xs.flatMap(x => x.ok ? [x.id] : [])
with() New array with one index replaced No [1,2,3].with(1, 9) // [1,9,3]

Gotcha: reduce without an initial value uses element 0 as the seed and throws on an empty array. Always pass the second argument; it also gives TypeScript something to infer from.

Iterate

Method Returns Mutates Example
forEach() undefined. Cannot break or return early No rows.forEach(r => log(r))
for...of Nothing. Supports break, continue, and await No for (const r of rows) { ... }
entries() Iterator of [index, value] pairs No for (const [i, v] of a.entries())
keys() Iterator of indices, including holes No [...Array(3).keys()] // [0,1,2]
values() Iterator of values, the default iterator No const it = a.values()
for...in String keys, including inherited ones. Do not use on arrays No for (const k in a) // k is "0", not 0

Gotcha: forEach ignores a returned promise, so arr.forEach(async x => await save(x)) finishes instantly and swallows every rejection. Use for...of with await, or Promise.all(arr.map(...)).

Sort and order

Method Returns Mutates Example
sort() The same array, sorted in place. Stable since ES2019 Yes nums.sort((a, b) => a - b)
toSorted() A new sorted array No nums.toSorted((a, b) => b - a)
reverse() The same array, reversed in place Yes a.reverse()
toReversed() A new reversed array No messages.toReversed()
sort() by string Locale aware ordering with a collator Yes names.sort((a, b) => a.localeCompare(b))
sort() multi key Chain comparisons with logical OR Yes rows.sort((a,b) => a.g - b.g || a.n - b.n)

Gotcha: a bare sort() converts every element to a string first, so [10, 9, 1].sort() gives [1, 10, 9]. Numbers always need a comparator.

Combine, slice, and stringify

Method Returns Mutates Example
concat() New array of both, one level flattened No [1].concat([2,3]) // [1,2,3]
slice() Shallow copy of a range, end exclusive No a.slice(-3) // last three
slice() Full shallow copy with no arguments No const copy = a.slice()
join() String. null and undefined become empty No ["a","b"].join("-") // "a-b"
toString() Comma joined string No [1,2].toString() // "1,2"
[...a, ...b] New array, and works with any iterable No [...a, extra, ...b]
[...new Set(a)] Deduplicated array, order preserved No [...new Set([1,1,2])] // [1,2]
Object.fromEntries() Object built from pairs No Object.fromEntries(a.map(x => [x.id, x]))

Gotcha: concat flattens one level of array arguments but not of nested elements, so [1].concat([[2]]) gives [1, [2]]. Spread has the same behavior and is easier to read.

Async patterns over arrays

There is no async version of map or filter. You combine map with a promise combinator, and the combinator you choose is the whole decision.

// Parallel, all must succeed. First rejection wins and the rest keep running.
const users = await Promise.all(ids.map(id => fetchUser(id)));

// Parallel, collect every outcome. Never rejects.
const results = await Promise.allSettled(ids.map(fetchUser));
const ok = results.filter(r => r.status === "fulfilled").map(r => r.value);

// First success wins; rejects only if every one fails.
const fastest = await Promise.any(mirrors.map(fetchFrom));

// First settled outcome wins, success or failure. Usually a timeout race.
const raced = await Promise.race([fetchUser(id), timeout(3000)]);

// Sequential, because order or rate limits matter.
const out = [];
for (const id of ids) out.push(await fetchUser(id));

// Async filter: map to predicates in parallel, then zip.
const flags = await Promise.all(items.map(isValid));
const valid = items.filter((_, i) => flags[i]);

// Bounded concurrency without a library: chunk, then run each chunk in parallel.
const chunk = (a, n) =>
  Array.from({ length: Math.ceil(a.length / n) }, (_, i) => a.slice(i * n, i * n + n));
for (const batch of chunk(ids, 5)) await Promise.all(batch.map(fetchUser));

// Drain an async iterable straight into an array.
const lines = await Array.fromAsync(readLines(file));

Gotcha: ids.map(async id => ...) returns an array of promises, not values. Forgetting the await Promise.all(...) around it produces an array full of Promise { pending }, which usually surfaces much later as a rendering bug.

Gotcha: callbacks receive three arguments. ["1","7","11"].map(parseInt) gives [1, NaN, 3] because the index is passed as the radix. Wrap it: .map(s => parseInt(s, 10)), or use .map(Number).

Keep going

Typing these correctly is its own skill: the TypeScript cheatsheet covers the utility types, narrowing, and generic constraints that make filter and reduce infer the shapes you actually wanted.

Working with array state inside components? The React hooks cheatsheet covers the dependency and identity rules that make the immutable four worth using, and Zustand vs Redux covers where that state should live.

Reference: MDN Array. More quick references are in the cheatsheet index.