Skip to main content

Command Palette

Search for a command to run...

Flattening Nested Arrays: The Complete Guide

Updated
8 min read
Flattening Nested Arrays: The Complete Guide
P
Backend developer exploring AI agents, backend systems, and architectural rabbit holes. I enjoy understanding how things work under the hood and occasionally over-engineering side projects for fun

Nested arrays are everywhere in real-world JavaScript applications—from deeply structured API responses to hierarchical data trees. Whether you're working with comment threads, file systems, or organizational charts, knowing how to flatten nested arrays efficiently can save you hours of debugging.

This guide goes beyond the basics. We'll explore edge cases that trip up most developers, performance pitfalls that can crash your app, and practical patterns you can use immediately in production code.


What You'll Learn

  • How to flatten arrays manually and with built-in methods

  • Handling tricky edge cases: empty slots, null, undefined, and depth limits

  • When flattening is a good idea—and when it's a trap

  • Writing stack-safe flattening for deeply nested data


1. The Basics: Understanding Array Flattening

The Core Concept

A nested array like [1, [2, [3, 4]], 5] becomes a flat array: [1, 2, 3, 4, 5]

The golden rule is simple:

If an element is an array, replace it with its own flattened contents. Otherwise, keep the element as-is.

The Simple Recursive Approach

Here's the starter pattern that many developers begin with:

function flatten(arr) {
  let result = [];
  for (let item of arr) {
    if (Array.isArray(item)) {
      result.push(...flatten(item));
    } else {
      result.push(item);
    }
  }
  return result;
}

// Usage
flatten([1, [2, [3, 4]], 5]); // → [1, 2, 3, 4, 5]

This works for basic cases, but the devil is in the details. Let's look at what breaks it.


2. The Tricky Edge Cases You Probably Missed

Most flattening implementations work fine until they don't. Here are the gotchas:

Empty Slots (Holes in Arrays)

JavaScript arrays can have "holes"—missing indices that are neither undefined nor null.

const holey = [1, , [2, , 3], 4];

// Most manual implementations
flatten(holey); // → [1, 2, 3, 4]

// Built-in method
[1, , [2, , 3], 4].flat(Infinity); // → [1, 2, 3, 4]

Why the difference?
for...of loops and forEach skip empty slots intentionally. If you need to preserve holes, you must use a traditional for loop with an in operator check.

function flattenPreservingHoles(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    if (!(i in arr)) {
      // Create hole in result
      continue;
    }
    const elem = arr[i];
    if (Array.isArray(elem)) {
      result.push(...flattenPreservingHoles(elem));
    } else {
      result.push(elem);
    }
  }
  return result;
}

Falsy Values vs. Empty Arrays

A common mistake: treating falsy values (0, null, false, undefined) as "empty":

flatten([0, [null, [false, undefined]]]);

❌ Wrong approach:

function flatten(arr) {
  return arr.reduce((acc, elem) => {
    if (!elem) return acc; // ← TRAP: skips 0 and false!
    return Array.isArray(elem) ? [...acc, ...flatten(elem)] : [...acc, elem];
  }, []);
}

✅ Correct approach:

function flatten(arr) {
  return arr.reduce((acc, elem) => {
    if (Array.isArray(elem)) {
      return [...acc, ...flatten(elem)];
    }
    return [...acc, elem]; // Always push, even falsy values
  }, []);
}

flatten([0, [null, [false, undefined]]]);
// → [0, null, false, undefined] ✓

Depth Limits (Partial Flattening)

Not all flattening needs to be complete. Sometimes you only want to flatten one or two levels:

function flattenDepth(arr, depth) {
  if (depth === 0) return arr.slice(); // Return copy without flattening
  
  let result = [];
  for (let item of arr) {
    if (Array.isArray(item) && depth > 0) {
      result.push(...flattenDepth(item, depth - 1));
    } else {
      result.push(item);
    }
  }
  return result;
}

const nested = [1, [2, [3, [4, 5]]]];

flattenDepth(nested, 0); // → [1, [2, [3, [4, 5]]]] (no change)
flattenDepth(nested, 1); // → [1, 2, [3, [4, 5]]]
flattenDepth(nested, 2); // → [1, 2, 3, [4, 5]]
flattenDepth(nested, Infinity); // → [1, 2, 3, 4, 5]

Key insight: depth = 0 returns a copy of the original array, not a "single array"—this subtle distinction matters!


3. Real-World Scenario: Extracting Nested Comment IDs

Let's say your API returns a comment thread with nested replies:

{
  "comments": [
    {
      "id": 1,
      "text": "Great post!",
      "replies": [
        {
          "id": 2,
          "text": "I agree",
          "replies": []
        }
      ]
    }
  ]
}

You might think: "I'll just flatten the comments array!" But that's not quite right. You need to extract IDs from a tree structure, not flatten an array.

function getAllIds(comments) {
  let ids = [];
  for (let comment of comments) {
    ids.push(comment.id);
    
    if (comment.replies && comment.replies.length) {
      ids.push(...getAllIds(comment.replies));
    }
  }
  return ids;
}

getAllIds(response.comments); // → [1, 2, ...]

The lesson: Don't flatten just because you see nesting. Extract exactly what you need. Generic flattening is often the wrong tool for hierarchical data.


4. Performance: When Recursion Fails

Here's a scenario that breaks most developers' code: what if your nesting is 50,000 levels deep?

// Creates deeply nested array
let arr = [1];
for (let i = 0; i < 50000; i++) {
  arr = [arr];
}

flatten(arr); // → RangeError: Maximum call stack size exceeded

Why? Each recursive call adds a frame to the call stack. Eventually, you hit the limit.

The Solution: Iterative Flattening with a Heap-Based Stack

Instead of using the call stack, use an explicit array as your stack:

function flattenIterative(arr) {
  const stack = [...arr]; // Initialize stack with all items
  const result = [];
  
  while (stack.length) {
    const next = stack.pop();
    
    if (Array.isArray(next)) {
      // Push inner items back (order reversed, but we'll fix that)
      stack.push(...next);
    } else {
      // Use unshift to preserve original order
      result.unshift(next);
    }
  }
  
  return result;
}

// Now handles deeply nested arrays without stack overflow

Trade-off: This uses heap memory instead of the call stack, so it can handle millions of nesting levels—but unshift is O(n) per operation, making overall complexity O(n²). For production, prefer a generator pattern.

Better: Lazy Flattening with Generators

Don't build the entire flat array upfront. Yield items one by one:

function* flattenLazy(arr) {
  for (let item of arr) {
    if (Array.isArray(item)) {
      yield* flattenLazy(item);
    } else {
      yield item;
    }
  }
}

// Use it
for (let item of flattenLazy(deeplyNested)) {
  console.log(item);
}

5. Transform While Flattening

Often you want to flatten and transform in one pass. This is more efficient than doing it in two operations:

function flattenAndMultiply(arr) {
  return arr.reduce((acc, elem) => {
    if (Array.isArray(elem)) {
      acc.push(...flattenAndMultiply(elem));
    } else if (elem !== undefined) {
      acc.push(elem * 2);
    }
    return acc;
  }, []);
}

flattenAndMultiply([1, [2, [3, 4]], 5]);
// → [2, 4, 6, 8, 10]

The key: check for undefined to skip holes, but still process falsy values like 0 and null.


6. Removing Holes and Filtering Values

Sometimes you need more control over what gets included:

Option A: Keep null/undefined, Remove Holes Only

function flattenNoHoles(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    if (!(i in arr)) continue; // Skip hole
    
    const elem = arr[i];
    if (Array.isArray(elem)) {
      result.push(...flattenNoHoles(elem));
    } else {
      result.push(elem);
    }
  }
  return result;
}

const mixed = [1, , [null, , undefined]];
flattenNoHoles(mixed); // → [1, null, undefined]

Option B: Remove Holes AND null/undefined

function flattenCompact(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    if (!(i in arr)) continue; // Skip hole
    
    const elem = arr[i];
    if (Array.isArray(elem)) {
      result.push(...flattenCompact(elem));
    } else if (elem !== null && elem !== undefined) {
      result.push(elem);
    }
  }
  return result;
}

const mixed = [1, , [null, , undefined]];
flattenCompact(mixed); // → [1]

7. When NOT to Flatten: The File System Example

Flattening everything sounds simple, but it can be a disaster. Consider a file system with millions of files:

// ❌ BAD: Flatten entire tree
function findAllFiles(root) {
  let files = [];
  
  function traverse(node) {
    if (node.isFile) {
      files.push(node);
    } else {
      for (let child of node.children) {
        traverse(child);
      }
    }
  }
  
  traverse(root);
  return files; // Entire array in memory!
}

Problems:

  • Memory explosion: 10 million files = 10 million objects in RAM

  • Slow startup: You traverse the entire tree before returning anything

  • Wasted work: If you're searching for something, you process files you don't need

Better Approach 1: Lazy Flattening with Generators

function* walkFiles(node) {
  if (node.isFile) {
    yield node;
  } else {
    for (let child of node.children) {
      yield* walkFiles(child);
    }
  }
}

// Use it
for (let file of walkFiles(root)) {
  if (file.name.includes('bug.js')) {
    console.log('Found:', file);
    break; // No need to traverse further
  }
}

Better Approach 2: Database with Indexing

For truly large datasets, store files in a flat database table with parent references:

SELECT * FROM files 
WHERE type = 'file' AND name LIKE '%bug.js%';

Better Approach 3: Virtual Scrolling

For UI applications, fetch and flatten in chunks:

async function* getFilesInChunks(root, chunkSize = 100) {
  let buffer = [];
  
  for (let file of walkFiles(root)) {
    buffer.push(file);
    if (buffer.length >= chunkSize) {
      yield buffer;
      buffer = [];
    }
  }
  
  if (buffer.length > 0) yield buffer;
}

Further Reading


Ready to level up? Practice with your own deeply nested objects, experiment with depth limits, and always consider whether flattening is the right tool for the job.