#Arrays#Recursion

Flatten Nested Array in JavaScript

Implement a function to flatten a nested array in JavaScript.

By Pratik RaiMedium

Flattening a nested array is the canonical recursion exercise, and it is asked constantly because it has an obvious elegant answer, a non-obvious failure mode, and a natural progression of follow-up questions. Array.prototype.flat solves it natively — but the interview is about what flat is doing underneath.

For the spec-accurate flat implementation including holes and depth handling, see the Array.prototype.flat polyfill. This page is about the underlying algorithms and their trade-offs.

The recursive answer

JSfile.javascript
1function flatten(input) { 2 const result = []; 3 for (const item of input) { 4 if (Array.isArray(item)) { 5 result.push(...flatten(item)); 6 } else { 7 result.push(item); 8 } 9 } 10 return result; 11}

Short and readable. Array.isArray is the correct test rather than instanceof Array, which returns false for arrays originating in another realm — an iframe, a worker, a Node vm context. Those are genuine arrays that instanceof misses.

A reduce variant reads more functionally and does the same work:

JSfile.javascript
1const flatten = (input) => 2 input.reduce( 3 (acc, item) => acc.concat(Array.isArray(item) ? flatten(item) : item), 4 [] 5 );

Be aware this one is quadratic: concat allocates a new array on every element rather than appending in place. Elegant for a whiteboard, wrong for a large input.

Why recursion eventually breaks

Each nesting level consumes a stack frame. Engines cap stack depth at roughly ten to fifteen thousand frames, so a sufficiently deep structure throws:

JSfile.javascript
1let deep = [1]; 2for (let i = 0; i < 50000; i++) deep = [deep]; 3flatten(deep); // RangeError: Maximum call stack size exceeded

Note this is about depth, not size. A million-element flat array is fine; a fifty-thousand-deep nest is not. Real data — JSON payloads, component trees, category hierarchies — is rarely more than a handful deep, so recursion is usually the right call. It becomes a liability when the shape is unbounded or comes from outside your system.

The iterative version

Replace the call stack with an explicit stack on the heap, which is bounded by memory rather than frame count:

JSfile.javascript
1function flattenIterative(input) { 2 const stack = [...input]; 3 const result = []; 4 5 while (stack.length) { 6 const item = stack.pop(); 7 if (Array.isArray(item)) { 8 stack.push(...item); 9 } else { 10 result.push(item); 11 } 12 } 13 14 return result.reverse(); // popping from the end reverses order 15}

The reverse() at the end is the price of using pop, which is O(1). Using shift instead would preserve order naturally but is O(n) per operation, making the whole thing quadratic. Reversing once at the end is O(n) and clearly the better trade.

stack.push(...item) reintroduces the argument-count limit — for very wide inner arrays, loop instead of spreading.

Depth-limited flattening

Real code usually wants control over how far to go. Carry the remaining depth alongside each value:

JSfile.javascript
1function flattenDepth(input, depth = 1) { 2 const result = []; 3 for (const item of input) { 4 if (depth > 0 && Array.isArray(item)) { 5 result.push(...flattenDepth(item, depth - 1)); 6 } else { 7 result.push(item); 8 } 9 } 10 return result; 11}

This mirrors native flat, whose default depth is 1 — a detail that surprises people expecting full flattening. Use flat(Infinity) for that.

Approaches to avoid

arr.toString().split(',') works only for arrays of strings and numbers, and destroys types — every element comes back a string, null becomes an empty string, and nested undefined vanishes. It appears in blog posts as a clever trick; do not ship it.

JSON.parse('[' + JSON.stringify(arr).replace(/[\[\]]/g, '') + ']') is worse: it breaks on any string containing a bracket.

Both are the kind of answer that looks smart and fails a follow-up question about a string containing a comma.

Complexity

All correct versions are O(n) in the total number of elements across every level — each is visited once. The differences are constant factors and memory:

  • Recursive: O(d) stack space for depth d.
  • Iterative: O(n) heap space for the working stack.
  • concat-based reduce: O(n²) time — avoid on large inputs.

Common follow-ups

"Flatten an object instead." Turn {a: {b: {c: 1}}} into {'a.b.c': 1}. Same traversal, but you accumulate a key path and must decide how to treat arrays and nulltypeof null === 'object' will catch you out.

"Handle circular references." Track visited arrays in a WeakSet and skip or throw on revisit. Native flat does not do this and will hang.

"Flatten lazily." A generator yields values without materialising the whole result, which matters when the caller only needs the first few:

JSfile.javascript
1function* flattenLazy(input) { 2 for (const item of input) { 3 if (Array.isArray(item)) yield* flattenLazy(item); 4 else yield item; 5 } 6}

yield* delegating to a recursive generator is the neatest expression of the algorithm, though it still consumes stack depth per level.

Key takeaways

  • Recursion is clearest and correct for realistic data depths.
  • An explicit stack survives unbounded nesting; recursion does not.
  • Use Array.isArray, not instanceof, for cross-realm safety.
  • Avoid concat in a reducer — it is quadratic.
  • Native flat defaults to depth 1; pass Infinity for full flattening.
  • String-based tricks destroy types and break on real data.

Goal: Implement a function to flatten a nested array in JavaScript.

Frequently asked questions

Why is flattening an array such a common interview question?
Because it is the smallest problem that requires recursion over a structure of unknown depth, and it has an obvious iterative alternative. The interviewer gets to see whether you can write both and say which you would ship.
What is the recursive solution, and where does it break?
For each element, recurse if it is an array and push otherwise — four lines that read exactly like the definition. It breaks on very deep nesting, where each level adds a stack frame and a few thousand levels exhaust the call stack. That is the answer to "what is wrong with this?", and it is why the iterative version exists.
How do you flatten iteratively?
With an explicit stack: push the array, pop items, and push array elements back on rather than recursing. Depth then costs heap memory instead of stack frames. The common version pops from the end and reverses at the finish, which is worth explaining rather than leaving as a mysterious `reverse()`.
What should you use in real code?
`Array.prototype.flat(Infinity)` — native, handles sparse arrays correctly, and faster for everybody to read. The exercise is about demonstrating recursion, not distrusting the standard library. Say so, and implement it anyway.

Related Challenges

Continue learning with these related challenges

View All
JavaScript

Array.prototype.reduce Polyfill

Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.

JavaScript · Arrays · PolyfillsPratik Rai ·

JavaScript

Array.prototype.flat Polyfill

Implement Array.prototype.flat from scratch with a configurable depth. Default depth is 1, not Infinity.

JavaScript · Arrays · RecursionPratik Rai ·

JavaScript

Deep Equality

Compare two values structurally, because === only ever compares references.

JavaScript · ES6Pratik Rai ·