Array Methods Interview Questions & Answers

32 questions Updated 2026-06-18

JavaScript array method interview questions — map, filter, reduce, forEach, some, every, find, flat, flatMap, Array.from and how to chain iteration methods effectively.

Read the in-depth guideJavaScript Array Methods Explained — map, filter, reduce and the Iteration Toolkit

map creates a new array by calling a callback on every element and collecting the return values. It does not mutate the original and always returns an array of the same length.

const nums = [1, 2, 3]
const doubled = nums.map(n => n * 2) // [2, 4, 6]
nums // [1, 2, 3] — unchanged

A common pitfall is using map purely for side effects (logging, pushing elsewhere). If you don't use the returned array, use forEach instead — it signals intent and avoids allocating a throwaway array.

filter returns a new array containing only the elements for which the callback returns a truthy value. Length is less than or equal to the original.

const nums = [1, 2, 3, 4]
const evens = nums.filter(n => n % 2 === 0) // [2, 4]

Gotcha: the predicate must return a boolean-ish value. Forgetting return in a block body silently filters everything out, since undefined is falsy:

nums.filter(n => { n % 2 === 0 }) // [] — no return

forEach runs a callback once per element for side effects. It always returns undefined — you can't chain off it.

[1, 2, 3].forEach(n => console.log(n))

Two gotchas: you cannot break out of forEach (use a for...of or some if you need early exit), and it skips holes in sparse arrays. If you need a transformed result, reach for map instead.

reduce folds an array into a single value by threading an accumulator through a callback (acc, cur) => newAcc. You pass an initial value as the second argument.

const sum = [1, 2, 3, 4].reduce((acc, n) => acc + n, 0) // 10

Pitfall: omitting the initial value makes reduce use the first element as the seed and start at index 1 — and it throws on an empty array. Always pass an explicit initial value unless you have a specific reason not to.

It throws a TypeError: Reduce of empty array with no initial value. With no seed, reduce needs at least one element to start from.

[].reduce((a, b) => a + b)    // TypeError
[].reduce((a, b) => a + b, 0) // 0

This is why supplying an initial value is a best practice — it makes the operation total (defined for all inputs) and pins down the accumulator's type.

reduceRight is identical to reduce but processes elements from right to left. It matters when the operation is not associative or when order of composition matters.

// function composition: f(g(h(x)))
const compose = (...fns) => x =>
  fns.reduceRight((acc, fn) => fn(acc), x)

const f = compose(n => n + 1, n => n * 2)
f(3) // 7  -> (3*2)+1

For plain addition the direction is irrelevant; for string building, division, or composition it changes the result.

some returns true if at least one element passes the predicate; every returns true only if all elements pass. Both short-circuit.

[1, 2, 3].some(n => n > 2)  // true  (stops at 3)
[1, 2, 3].every(n => n > 0) // true
[1, 2, 3].every(n => n > 1) // false (stops at 1)

Edge case: every on an empty array returns true (vacuous truth) and some returns false. This trips people up in validation code.

find returns the first element that satisfies the predicate, or undefined. findIndex returns its index, or -1.

const users = [{ id: 1 }, { id: 2 }]
users.find(u => u.id === 2)      // { id: 2 }
users.findIndex(u => u.id === 2) // 1
users.find(u => u.id === 9)      // undefined

Use find when you want the object itself; use findIndex when you need the position (e.g. to splice or replace it).

They mirror find/findIndex but search from the end of the array backward, returning the last matching element or its index.

const nums = [1, 5, 3, 5, 2]
nums.findLast(n => n === 5)      // 5  (the second 5)
nums.findLastIndex(n => n === 5) // 3

Before these (ES2023) you'd reverse a copy or loop manually. They avoid an extra [...arr].reverse().find(...) allocation and keep the original index meaningful.

Use indexOf for primitive equality (it uses strict ===), and find when you need a predicate (matching by a property or condition).

[10, 20, 30].indexOf(20)              // 1
users.find(u => u.name === 'Ann')     // predicate match

indexOf can't match objects by content, only by reference, so for arrays of objects you almost always want find/findIndex.

flat returns a new array with sub-array elements concatenated up to a given depth (default 1). It does not mutate.

[1, [2, [3, [4]]]].flat()        // [1, 2, [3, [4]]]
[1, [2, [3, [4]]]].flat(2)       // [1, 2, 3, [4]]
[1, [2, [3]]].flat(Infinity)     // [1, 2, 3] fully flatten

Use flat(Infinity) for arbitrarily nested structures. It also removes empty slots in sparse arrays as a side effect.

flatMap maps each element then flattens the result by one level, in a single pass. It's equivalent to map(...).flat() but more efficient and expressive.

const sentences = ['a b', 'c d']
sentences.flatMap(s => s.split(' ')) // ['a', 'b', 'c', 'd']

It only flattens one level. A neat trick: returning [] from the callback drops an element, letting flatMap act as a combined map+filter.

Array.from creates a real array from any iterable or array-like object (one with a length and indexed keys). It accepts an optional mapping function as a second argument.

Array.from('abc')              // ['a', 'b', 'c']
Array.from(new Set([1, 1, 2])) // [1, 2]
Array.from({ length: 3 }, (_, i) => i) // [0, 1, 2]

The mapping form is handy for generating sequences and avoids creating an intermediate array, unlike [...iterable].map(fn).

Array.of creates an array from its arguments consistently, fixing a quirk of new Array(): a single numeric argument is treated as a length, not an element.

Array(3)      // [ <3 empty items> ] — length 3
Array.of(3)   // [3]
Array(1, 2)   // [1, 2] — but inconsistent with Array(3)
Array.of(1, 2)// [1, 2]

Array.of always treats arguments as elements, so it's safer in generic code.

They return array iterators, not arrays. keys() yields indices, values() yields elements, and entries() yields [index, value] pairs.

const arr = ['a', 'b']
for (const [i, v] of arr.entries()) {
  console.log(i, v) // 0 'a', then 1 'b'
}
[...arr.keys()]   // [0, 1]
[...arr.values()] // ['a', 'b']

entries() is the clean way to get the index inside a for...of loop without a manual counter.

Because map, filter, slice, and friends each return a new array, you can chain them into a readable pipeline.

const result = users
  .filter(u => u.active)
  .map(u => u.name)
  .sort() // active users' names, sorted

Tradeoff: each step allocates a new array and does a full pass. For huge arrays or hot paths a single reduce (or a plain loop) can avoid the intermediate arrays, at the cost of readability.

Seed the accumulator with {} and assign keys as you go. This is the canonical way to index a list by id.

const users = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
const byId = users.reduce((acc, u) => {
  acc[u.id] = u
  return acc
}, {}) // { 1: {...}, 2: {...} }

For this exact case, Object.fromEntries(users.map(u => [u.id, u])) is often clearer. Reserve reduce-to-object for logic that doesn't fit a simple map.

Use reduce to accumulate into buckets, initializing each bucket lazily.

const items = [{ type: 'a', v: 1 }, { type: 'b', v: 2 }, { type: 'a', v: 3 }]
const grouped = items.reduce((acc, item) => {
  (acc[item.type] ??= []).push(item) // create bucket then push
  return acc
}, {})
// { a: [{a,1},{a,3}], b: [{b,2}] }

Modern engines also offer Object.groupBy(items, item => item.type), which does exactly this without the boilerplate.

Concatenate each sub-array into an accumulator:

const nested = [[1, 2], [3], [4, 5]]
const flat = nested.reduce((acc, sub) => acc.concat(sub), []) // [1,2,3,4,5]

In practice prefer the dedicated nested.flat() — it's clearer and faster. The reduce version is mostly useful as an interview demonstration of how flattening works under the hood, or when you need custom merge logic per chunk.

Every iteration callback receives (element, index, array). The second parameter is the index.

['a', 'b', 'c'].map((char, i) => `${i}:${char}`) // ['0:a', '1:b', '2:c']

Gotcha: passing a method like arr.map(parseInt) breaks because parseInt receives the index as its radix argument: ['1','2','3'].map(parseInt) gives [1, NaN, NaN]. Wrap it: arr.map(Number) or arr.map(s => parseInt(s, 10)).

Choose map when you want a transformed array back; choose forEach when you only need side effects and ignore the return value.

const upper = names.map(n => n.toUpperCase()) // need result
names.forEach(n => console.log(n))            // side effect only

Using map without consuming the result is a code smell — it implies a transformation that's thrown away, confusing readers and wasting an allocation.

Each chained method (filter().map()) does a full pass and allocates a new array. A single for loop or one reduce does one pass with no intermediate arrays.

// 2 passes, 2 arrays
arr.filter(x => x > 0).map(x => x * 2)
// 1 pass, 1 array
arr.reduce((acc, x) => (x > 0 && acc.push(x * 2), acc), [])

For typical data sizes the difference is negligible and readability wins. Only fuse into a loop when profiling shows it matters.

Iteration methods on an empty array simply don't call the callback and return an empty (or seeded) result — no errors.

[].map(x => x.boom)        // [] — callback never runs
[].filter(Boolean)         // []
[].reduce((a, b) => a + b) // throws — no initial value

The only landmine is reduce with no initial value, which throws on empty input. Everything else degrades gracefully to an empty result.

Match the method to the shape of the result you want:

transform each -> map
keep a subset -> filter
one value -> reduce
a single match -> find / findIndex
yes/no across all -> some / every
side effects only -> forEach (or for...of)

Picking the right method makes intent obvious. Reaching for reduce when map or filter fits is a common over-engineering mistake.

Put each step on its own line, name intermediate results when a chain gets long, and keep callbacks small (extract named functions for complex logic).

const activeNames = users
  .filter(isActive)      // named predicate
  .map(u => u.name)
  .sort(byAlpha)

A chain longer than 3-4 steps, or with multi-line callbacks, is often clearer split into named variables. Optimize for the next reader, not for fewer lines.

map produces a new array, but the elements are the same references for objects — it's a shallow transform.

const arr = [{ n: 1 }]
const copy = arr.map(o => o)
copy[0].n = 99
arr[0].n // 99 — same object reference

To copy each object too, map to a new object: arr.map(o => ({ ...o })). For deeply nested data, structuredClone per element is safer.

map is meant to be a pure transformation: input array in, new array out. Mutating external state or other elements inside the callback makes the code hard to reason about and order-dependent.

let total = 0
arr.map(x => { total += x; return x }) // hidden side effect

Use forEach/for...of for side effects and reduce for accumulation. Keep map pure so chains stay predictable and refactor-safe.

filter walks the entire array building a new one, then find walks again. If you only need the first match, find alone short-circuits on the first hit.

arr.filter(x => x > 10)[0]   // full pass + array allocation
arr.find(x => x > 10)        // stops at first match

Similarly, prefer some over filter(...).length > 0. Choosing a short-circuiting method avoids unnecessary work on large arrays.

Thread the running maximum through the accumulator:

const max = [3, 7, 2, 9, 4].reduce((m, n) => (n > m ? n : m), -Infinity)
// 9

Seeding with -Infinity keeps it correct even when all values are negative. For plain numbers Math.max(...arr) is shorter, but it risks a stack overflow on very large arrays — reduce stays safe there.

Use Array.isArray. typeof [] returns 'object', so it can't distinguish arrays from other objects.

Array.isArray([1, 2]) // true
Array.isArray('ab')   // false
typeof []             // 'object' — useless here

Array.isArray also works correctly across iframes/realms, where instanceof Array can fail because each realm has its own Array constructor.

forEach ignores the promise returned by an async callback, so it does not wait — the loop finishes before any async work completes.

ids.forEach(async id => { await save(id) }) // doesn't await
// "done" logs before saves finish

for (const id of ids) { await save(id) }    // sequential
await Promise.all(ids.map(id => save(id)))  // parallel

Use a for...of loop for sequential awaits, or map + Promise.all to run them concurrently and await the lot.

It's the array being iterated itself. It lets a callback reference neighboring elements without closing over an outer variable.

[10, 20, 30].map((val, i, arr) =>
  i > 0 ? val - arr[i - 1] : 0
) // [0, 10, 10] — differences

It's rarely needed, but handy for look-ahead/look-behind logic and for writing generic helpers that don't depend on an external reference to the array.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.