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.

0
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.