Mastering Recursion in JavaScript: A Step-by-Step Guide to Flatten Deeply Nested Arrays


When working with complex data structures in JavaScript, especially deeply nested arrays of objects, it's easy to get overwhelmed. But with the power of recursion and structured problem-solving, we can break down any problem into manageable steps. This article walks you through designing a robust function that flattens nested comment threads by thinking in chunks, handling edge cases, and maintaining clean logic.
Problem Statement
We have a dataset like this:
const data = [
{
id: 1,
message: "Very Good",
child: [
{
id: 2,
message: "Thank you",
child: [
{
id: 3,
message: "You are Welcome"
},
{}, // empty object
null
]
},
{},
null
]
},
{},
"abc",
null,
[],
{
id: 4,
message: "Another level",
child: [] // empty child array
},
{
id: 5,
message: "Testing invalid child",
child: "not an array" // invalid child type
},
{
id: 6,
// missing message
child: [
{
id: 7,
message: "Nested but valid"
}
]
},
{
message: "Missing ID" // missing id
}
];
Our goal is to extract and flatten all valid comments with id
and message
properties into a single array, ignoring invalid or malformed entries.
Step-by-Step Thinking: Building the flatInDepth
Function
const flatInDepth = (comments) => {
const newFlatArray = [];
// =========== Checkpoint 1 ===========
// 1. Ensure input is an array.
// 2. Ensure it's not an empty array.
// =========== Checkpoint 2 ===========
// 3. Check for null or undefined.
// 4. Ensure it's an object.
// 5. Ensure it has minimum two properties.
// =========== Checkpoint 3 ===========
// 6. Skip if id or message is null or undefined
// =========== Checkpoint 4 ===========
// 7. Recursively flatten all the valid children
return newFlatArray;
}
Here all the steps and edge cases that need to be concern. Let explore all the steps/checkpoint, why they are important.
Checkpoint 1 : Is the input a non-empty array?
const flatInDepth = (comments) => {
const newFlatArray = [];
// =========== Checkpoint 1 ===========
// 1. Ensure input is an array.
// 2. Ensure it's not an empty array.
if (Array.isArray(comments) && comments.length > 0) {
// Proced ...
}
return newFlatArray;
}
Why important: Without verifying the input, the function could attempt to loop over non-iterable values like null
, undefined
, or plain objects, leading to runtime errors.
Without this check: We might see errors like comments.entries is not a function
or unnecessary recursion on invalid data.
Checkpoint 2 : Is each element a valid object?
const flatInDepth = (comments) => {
const newFlatArray = [];
// =========== Checkpoint 1 ===========
// 1. Ensure input is an array.
// 2. Ensure it's not an empty array.
if (Array.isArray(comments) && comments.length > 0) {
for (const [ind, comment] of comments.entries()) {
// =========== Checkpoint 2 ===========
// 3. Check for null or undefined.
// 4. Ensure it's an object.
// 5. Ensure it has minimum two properties.
if (
!comment ||
typeof comment !== "object" ||
Object.keys(comment).length < 1
)
continue;
// Proced ....
}
}
return newFlatArray;
}
Why important: Arrays often contain mixed or malformed entries, especially if the data comes from an external source (like an API). Skipping over nulls, empty objects, or strings protects against unpredictable errors.
Without this check: Trying to destructure or recurse into invalid types could throw runtime exceptions or produce incorrect output.
Checkpoint 3 : Are key properties (id
and message
) valid?
const flatInDepth = (comments) => {
const newFlatArray = [];
// =========== Checkpoint 1 ===========
// 1. Ensure input is an array.
// 2. Ensure it's not an empty array.
if (Array.isArray(comments) && comments.length > 0) {
for (const [ind, comment] of comments.entries()) {
// =========== Checkpoint 2 ===========
// 3. Check for null or undefined.
// 4. Ensure it's an object.
// 5. Ensure it has minimum two properties.
if (
!comment ||
typeof comment !== "object" ||
Object.keys(comment).length < 1
)
continue;
const { id, message, child } = comment;
// =========== Checkpoint 3 ===========
// 6. Skip if id or message is null or undefined
if (id == null || message == null) continue;
newFlatArray.push({ id, message });
}
}
return newFlatArray;
}
Why important: Not all objects are useful for our transformation. We only want to include comments that are meaningful and complete — that is, those with id
and message
.
Without this check: We risk pushing incomplete or broken data into our output array, which can confuse downstream consumers of our function.
Checkpoint 4 : Here the main part—Recursion.
const flatInDepth = (comments) => {
const newFlatArray = [];
// =========== Checkpoint 1 ===========
// 1. Ensure input is an array.
// 2. Ensure it's not an empty array.
if (Array.isArray(comments) && comments.length > 0) {
for (const [ind, comment] of comments.entries()) {
// =========== Checkpoint 2 ===========
// 3. Check for null or undefined.
// 4. Ensure it's an object.
// 5. Ensure it has minimum two properties.
if (
!comment ||
typeof comment !== "object" ||
Object.keys(comment).length < 1
)
continue;
const { id, message, child } = comment;
// =========== Checkpoint 3 ===========
// 6. Skip if id or message is null or undefined
if (id == null || message == null) continue;
newFlatArray.push({ id, message });
// =========== Checkpoint 4 ===========
// 7. Recursively flatten all the valid children
if (Array.isArray(child) && child.length > 0) {
const nested = flatInDepth(child);
newFlatArray.push(...nested);
}
}
}
return newFlatArray;
}
Why important: We should only recurse when the child
is a valid, non-empty array. This guards against infinite or unnecessary recursive calls.
Without this check: We might recurse into undefined
or empty data, wasting performance and possibly causing stack overflow in deeply nested structures.
Important !
if (Array.isArray(child) && child.length > 0) {
newFlatArray.push(...flatInDepth(child));
}
The spread operator (...
) in this line is crucial because it ensures that we're pushing individual elements returned from the recursive call into the main newFlatArray
array, rather than pushing the whole returned array as a nested array.
Without the spread operator, this would result in a nested array inside newFlatArray
, like:
[
{ id: 1, message: "Very Good" },
[
{ id: 2, message: "Thank you" },
[
{ id: 3, message: "You are Welcome" }
]
]
]
Which defeats the purpose of flattening the structure.
With the spread operator, It results in a flat array
The Result :
[
{ id: 1, message: "Very Good" },
{ id: 2, message: "Thank you" },
{ id: 3, message: "You are Welcome" }
//..........
]
Final Thoughts
This function does more than just flatten nested arrays. It shows the process of thinking like a developer:
Breaking a big problem into digestible chunks
Identifying edge cases and defensive checks
Using recursion responsibly
Keeping logic clear and maintainable
Learning to reason step-by-step, like we did with flatInDepth
, strengthens your ability to approach any intermediate-level challenge with confidence. Start with a clear goal, validate each part of the data, and build toward the full solution.
Subscribe to my newsletter
Read articles from Md. Monirul Islam directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Md. Monirul Islam
Md. Monirul Islam
Passionate and detail-oriented React Developer with over 5 years of industry experience in building scalable web and mobile applications using modern technologies such as React.js, Next.js, TypeScript, and Node.js. Proven track record of delivering high-quality solutions in the fintech, real estate, and government sectors.