JavaScript Array Flattening Guide
Six approaches, real benchmarks, and the mental model that makes nested data finally make sense.

π Hey, I'm Mohd Kaif β a student documenting my journey through code. I write about what I'm learning in real-time β the wins, the struggles, and the "aha!" moments. From JavaScript and React to backend systems with Node.js, databases, DevOps, TypeScript, and AI integrations. This blog is my public learning journal: honest, evolving, and always exploring. If you're curious about any of these topics, let's learn and build together!
There's a moment every JavaScript developer hits where they stare at their console output and think: why does my data look like a Russian nesting doll?
You've got an array. But inside it, there are more arrays. And inside those β you guessed it β more arrays. You know you need all the values in one clean list, but every approach you try either only half-works or blows up entirely.
Sound familiar? You might be wondering:
Why does
.flat()sometimes leave me with arrays still nested inside my result?How do I flatten an array without using built-in methods (for that interview next week)?
Which approach actually holds up on deeply nested, real-world data without crashing?
When is flattening actually the wrong move?
Here's the thing: the problem isn't that you don't understand arrays. Nested structures are genuinely counterintuitive, and JavaScript gives you six different ways to deal with them β each with different trade-offs. If you've ever felt confused by this, you're in exactly the right company.
This guide will make it click. For good.
β What You'll Learn
What nested arrays are and how to visualize them as tree structures
Why flattening is one of the most practical data transformation skills you can have
How to flatten arrays using 6 different approaches β from the one-liner to the interview-proof recursive solution
When each approach is the right tool for the job
Why some approaches silently destroy your data (and how to catch it)
How to handle the tricky edge cases that trip people up in code reviews and interviews
No prerequisites beyond basic JavaScript. If you know what an array is, you're good.
What Are Nested Arrays?
A nested array is simply an array that contains other arrays as elements β one or more levels deep.
// One level deep
const shallow = [1, [2, 3], [4, 5]];
// Multiple levels deep
const deep = [1, [2, [3, [4, [5]]]]];
// Mixed β this is what real API data often looks like
const messy = [1, [2, 3], [4, [5, 6]], [[7, 8], 9]];
The best mental model for this is a tree. The top-level array is the root. Each nested array is a branch. The actual values β numbers, strings, objects β are the leaves.
[1, [2, [3, 4], 5]]
root
/ | \ \
1 [ ] <- branch
|\ \
2 [ ] 5 <- another branch
| \
3 4 <- leaves
Your goal when flattening: collect every leaf, in order, into a single flat array.
[1, [2, [3, 4], 5]] β [1, 2, 3, 4, 5]
Why Does Flattening Actually Matter?
This isn't just an interview exercise. Nested arrays show up constantly in real work β often in ways you didn't design and can't control.
Normalizing API responses. Third-party APIs frequently return paginated or grouped data in nested structures:
// What the API gives you
const apiResponse = [
{ category: "Electronics", items: ["Phones", "Laptops"] },
{ category: "Clothing", items: ["Shirts", "Jeans"] }
];
// What you actually need for your dropdown
const allItems = apiResponse.flatMap(group => group.items);
// ["Phones", "Laptops", "Shirts", "Jeans"]
React rendering. If you're mapping over data to render components, nested arrays cause unexpected rendered output. Flat arrays are predictable; nested ones aren't.
State management. Deeply nested Redux or Zustand state is notoriously painful to update immutably. Normalizing it (flattening into a map by ID) is a standard performance technique.
Data pipelines. Any time you .map() a function that returns an array, you get an array of arrays. Flattening is the natural second step.
The pattern you'll hit constantly: map β flatten β continue. Once you internalize this, you start seeing it everywhere.
How Flattening Works: Thinking Through It Step by Step
Before jumping to code, let's build the intuition.
Take this array:
[1, [2, [3, 4], 5]]
Walk through it element by element and ask one question about each item: "Is this an array, or is this a value?"
Step 1: Look at 1 β It's a value. Collect it. Result: [1]
Step 2: Look at [2, ...] β It's an array. Go inside.
Step 3: Look at 2 β It's a value. Collect it. Result: [1, 2]
Step 4: Look at [3, 4] β It's an array. Go inside.
Step 5: Look at 3 β It's a value. Collect it. Result: [1, 2, 3]
Step 6: Look at 4 β It's a value. Collect it. Result: [1, 2, 3, 4]
Step 7: Look at 5 β It's a value. Collect it. Result: [1, 2, 3, 4, 5]
That's it. That's the entire algorithm:
If it's a value β collect it. If it's an array β go inside it and repeat.
This logic β apply a rule to yourself recursively β is the foundation of every approach below. The approaches differ only in how they implement this traversal.
6 Approaches to Flatten Arrays in JavaScript
1. Array.prototype.flat() β The Modern Standard
Introduced in ES2019, this is your everyday go-to.
const arr = [1, [2, [3, [4]]]];
arr.flat(); // [1, 2, [3, [4]]] β depth 1 (default)
arr.flat(2); // [1, 2, 3, [4]] β depth 2
arr.flat(Infinity); // [1, 2, 3, 4] β all the way down
Key behavior to know:
It does not mutate the original array β it returns a new one
flat(1)is the default; it only unwraps one levelflat(Infinity)handles arbitrary depth
When to use it: Any modern environment (Node 11+, all current browsers). This is the right default choice for production code.
When not to use it: Interviews that say "without using built-in methods." See approach #2.
2. Recursion β The Interview Essential
This is the approach you need to know cold. It directly implements the step-by-step logic from above.
function flattenArray(arr) {
let result = [];
for (const item of arr) {
if (Array.isArray(item)) {
// It's an array β go inside it (recursive call)
result = result.concat(flattenArray(item));
} else {
// It's a value β collect it
result.push(item);
}
}
return result;
}
flattenArray([1, [2, [3, [4]]]]);
// [1, 2, 3, 4]
Why interviewers love this: It tests whether you can decompose a problem, not just recall an API. The pattern β check type, recurse if nested, collect if not β generalizes to trees, DOM traversal, file systems, and more.
The trade-off: Deep recursion builds up a call stack. Flatten a structure nested 10,000 levels deep and you'll see:
RangeError: Maximum call stack size exceeded
For production code with uncontrolled nesting depth, use approach #4 instead.
3. reduce() β The Functional Approach
Same logic as recursion, expressed in a more declarative style. If you're comfortable with functional programming, this will feel natural.
function flatten(arr) {
return arr.reduce((accumulator, item) => {
return Array.isArray(item)
? accumulator.concat(flatten(item))
: accumulator.concat(item);
}, []);
}
flatten([1, [2, [3, 4]]]);
// [1, 2, 3, 4]
The mental model: reduce() builds up a result by processing each item one at a time. When an item is an array, you recurse into it and concat the flattened result. When it's a value, you concat it directly.
When to use it: When you're already in a functional pipeline and want to keep the chain going. Works well with other array methods.
The trade-off: Slightly harder to read for developers unfamiliar with reduce(), and shares the same call stack limitation as plain recursion.
4. Iterative Stack β The Production-Safe Approach
This is the solution when you can't trust how deep the nesting goes. It uses an explicit stack (just a regular array) instead of the call stack, so it won't overflow.
function flattenSafe(arr) {
const stack = [...arr];
const result = [];
while (stack.length > 0) {
const item = stack.pop();
if (Array.isArray(item)) {
// Push its contents back onto the stack to be processed
stack.push(...item);
} else {
result.push(item);
}
}
// We used pop(), so elements were processed right-to-left β reverse to restore order
return result.reverse();
}
flattenSafe([1, [2, [3, [4]]]]);
// [1, 2, 3, 4]
Why this works without stack overflow: Instead of the function calling itself (which grows the call stack), you're managing your own array as a stack. JavaScript's heap can handle this at any depth.
When to use it: Whenever nesting depth is user-controlled, comes from external data, or is otherwise unbounded.
5. flatMap() β The Underrated Workhorse
flatMap() is map() followed by flat(1) β but faster, because it's a single pass. It's arguably the most practically useful method of all once you see the pattern it solves.
// Without flatMap β awkward double step
const sentences = ["Hello World", "Foo Bar"];
const words = sentences.map(s => s.split(" ")).flat();
// ["Hello", "World", "Foo", "Bar"]
// With flatMap β clean single step
const words2 = sentences.flatMap(s => s.split(" "));
// ["Hello", "World", "Foo", "Bar"]
The pattern it solves: Any time your .map() callback returns an array, you get an array of arrays. flatMap() collapses that automatically.
// Real-world example: extracting all tags from multiple posts
const posts = [
{ title: "Post 1", tags: ["js", "arrays"] },
{ title: "Post 2", tags: ["react", "hooks"] },
{ title: "Post 3", tags: ["js", "performance"] }
];
const allTags = posts.flatMap(post => post.tags);
// ["js", "arrays", "react", "hooks", "js", "performance"]
The limit: Only flattens one level deep. For deeply nested structures, you still need .flat(Infinity) or the recursive approach.
6. toString() β The One to Know and Avoid
You'll occasionally see this trick in the wild. Understand it so you can recognize and fix it.
const arr = [1, [2, [3, 4]]];
const result = arr.toString().split(',').map(Number);
// [1, 2, 3, 4] β looks right!
Now watch what happens when your data isn't pure numbers:
// With strings containing commas
const arr2 = ["hello, world", [2, 3]];
arr2.toString().split(',').map(Number);
// [NaN, NaN, 2, 3] β silently destroyed your data
// With objects
const arr3 = [{ id: 1 }, [2, 3]];
arr3.toString().split(',');
// ["[object Object]", "2", "3"] β completely wrong
// With null/undefined
const arr4 = [1, null, [2, undefined]];
arr4.toString().split(',');
// ["1", "", "2", ""] β lost values silently
Why this matters: It appears to work for simple numeric arrays, which is exactly why it's dangerous. It will silently corrupt your data in any real-world scenario. Never use it in production code.
π οΈ Quick Reference: Which Approach When?
| Situation | Best Approach |
|---|---|
| Everyday production code, known-depth data | flat(Infinity) or flat(depth) |
| Map and flatten in one pass | flatMap() |
| Interview (no built-ins) | Recursion |
| Unknown/user-controlled nesting depth | Iterative stack |
| Functional pipeline | reduce() with recursion |
| Quick numeric prototype (throw away after) | toString() hack β but know its limits |
ποΈ Exercise 1: Warm Up
Before moving on, try this in your browser console:
const data = [1, [2, 3], [[4, 5], [6, [7, 8]]]];
// Try each of these and predict the output before running:
console.log(data.flat()); // What depth does this flatten to?
console.log(data.flat(2)); // What's still nested?
console.log(data.flat(Infinity));// The full result?
Then rewrite flat(Infinity) using the recursive approach from scratch β without looking at the code above.
Common Interview Scenarios (With Full Solutions)
Scenario 1: Flatten to a Specific Depth
You'll often be asked to implement flat(depth) from scratch. The key insight: depth is just a counter you decrement on each recursive call.
function flattenToDepth(arr, depth) {
// Base case: if depth is 0, stop flattening
if (depth === 0) return arr.slice(); // return a copy
return arr.reduce((acc, item) => {
if (Array.isArray(item)) {
return acc.concat(flattenToDepth(item, depth - 1));
}
return acc.concat(item);
}, []);
}
flattenToDepth([1, [2, [3, [4]]]], 1); // [1, 2, [3, [4]]]
flattenToDepth([1, [2, [3, [4]]]], 2); // [1, 2, 3, [4]]
flattenToDepth([1, [2, [3, [4]]]], 3); // [1, 2, 3, 4]
Scenario 2: Flatten and Transform in One Pass
A common follow-up: flatten the array and apply a transformation to each value.
// Flatten and double every value
function flattenAndTransform(arr, transform) {
return arr.reduce((acc, item) => {
if (Array.isArray(item)) {
return acc.concat(flattenAndTransform(item, transform));
}
return acc.concat(transform(item));
}, []);
}
flattenAndTransform([1, [2, [3]]], x => x * 2);
// [2, 4, 6]
// Works with any transform
flattenAndTransform([1, [2, [3]]], x => x + "!");
// ["1!", "2!", "3!"]
Scenario 3: Handle Edge Cases (What Interviewers Actually Want to See)
A senior-level answer accounts for these before being asked:
function flattenRobust(arr) {
// Guard: handle non-array input
if (!Array.isArray(arr)) return [arr];
return arr.reduce((acc, item) => {
if (Array.isArray(item)) {
return acc.concat(flattenRobust(item));
}
return acc.concat(item);
}, []);
}
// Empty arrays
flattenRobust([]); // []
// Sparse arrays (holes)
flattenRobust([1, , 3]); // [1, undefined, 3] β reduce skips holes
// Mixed types
flattenRobust([1, "two", [true, null, [undefined]]]);
// [1, "two", true, null, undefined]
// Already flat
flattenRobust([1, 2, 3]); // [1, 2, 3]
ποΈ Exercise 2: Write It From Memory
Close this tab. Open a blank JS file or browser console. Write a function called myFlatten that:
Takes an array and a depth parameter
Recursively flattens it to that depth
Returns a new array (doesn't mutate the original)
Handles empty arrays without crashing
Don't look at the code above. If you get stuck, look at just the description of the approach, not the implementation.
Performance: What the Numbers Actually Say
Claims about performance should come with evidence. Here's a comparison using console.time() on an array of 100,000 items nested 5 levels deep:
// Generate test data: [[[[[ ...100k items ]]]]]-ish
function generateNested(depth, size) {
let arr = Array.from({ length: size }, (_, i) => i);
for (let i = 0; i < depth; i++) arr = [arr];
return arr;
}
const testData = generateNested(5, 100000);
console.time("flat(Infinity)");
testData.flat(Infinity);
console.timeEnd("flat(Infinity)");
// ~3β8ms β native, fastest
console.time("Iterative stack");
flattenSafe(testData);
console.timeEnd("Iterative stack");
// ~15β30ms β pure JS, safe
console.time("Recursive");
flattenArray(testData);
console.timeEnd("Recursive");
// ~20β40ms β pure JS, may overflow on extreme depth
The takeaway:
flat()is the fastest β it's a native method optimized at the engine levelIterative stack is the safest for unknown nesting depths β no overflow risk
Recursive is fine for typical data but can fail on pathologically deep structures
Run this yourself β numbers vary by environment, but the relative ordering is consistent.
When Flattening Is the Wrong Answer
This is the question most tutorials skip. Sometimes the hierarchy isn't a bug β it's the point.
Don't flatten when:
You're working with a file system tree β the parent-child relationship matters
You have nested menu/nav structures β the nesting is the UI
Your data represents an org chart or category hierarchy β flattening loses meaning
You need to round-trip the data back to its original structure later
In these cases, what you actually want is tree traversal β visiting nodes while preserving the structure. Or normalization, where you flatten the data into a dictionary keyed by ID, preserving relationships through references rather than nesting.
The difference: flattening destroys structure. Sometimes that's what you want. Sometimes it isn't. Knowing which situation you're in is the real skill.
ποΈ Exercise 3: Real-World Challenge
You're given this API response:
const apiData = [
{
region: "North",
cities: [
{ name: "Oslo", districts: ["Frogner", "GrΓΌnerlΓΈkka"] },
{ name: "Bergen", districts: ["Bergenhus", "Fana"] }
]
},
{
region: "South",
cities: [
{ name: "Stavanger", districts: ["Eiganes", "Madla"] }
]
}
];
Task: Extract a flat array of all district names: ["Frogner", "GrΓΌnerlΓΈkka", "Bergenhus", "Fana", "Eiganes", "Madla"]
Try solving it with flatMap() chained twice. Then try it with the recursive approach. Which feels more readable to you?
(Hint for flatMap(): you'll chain it once for cities, once for districts.)
What to Learn Next
You've got array flattening down. Here's where it naturally leads:
1. Tree Traversal Algorithms Flattening a nested array is a simplified form of depth-first tree traversal. Learn DFS and BFS properly β they're the foundation for working with DOM trees, file systems, and graph problems.
2. Data Normalization Patterns Instead of flattening hierarchical data, normalization stores it flat with ID references. This is how Redux recommends structuring state, and how most database-backed UIs work internally.
3. Array.prototype.reduce() β Deep Dive You used reduce() here as a flattening tool. It's actually one of the most powerful array methods β you can implement map(), filter(), flat(), and flatMap() with it. Understanding it deeply changes how you think about data transformation.
4. Functional Programming Patterns in JavaScript Flattening, mapping, and reducing are core concepts in functional programming. Exploring compose(), pipe(), and immutable data patterns will level up how you architect JavaScript applications.
π¬ Got Questions?
Drop a comment below β whether it's something that didn't click, a variation you're trying to solve, or a different approach you've used in your own codebase. I'd love to discuss it.
Here are topics coming up in future articles:
Tree Traversal in JavaScript: DFS and BFS with real-world examples β DOM manipulation, folder structures, and org charts
Mastering
reduce(): How one method can replacemap(),filter(),flat(), and more β and when it shouldn'tNormalizing Nested State: The pattern used in production Redux apps to keep deeply nested data fast and updatable
JavaScript Interview Patterns: The 10 algorithm patterns that show up in 80% of frontend interviews β with worked examples
Found this helpful? Share it with a developer friend who's been avoiding nested arrays β they'll thank you later. And if there's a topic you'd love to see covered next, drop it in the comments.
Happy coding. π




