Complete JavaScript Course Module 5: Beginner: Functions

JavaScript Recursion Explained with Examples

  • Beginner
  • Lesson 5.6
  • 7 min read
  • 22 of 76 in the course

In short

JavaScript recursion is when a function calls itself to solve a problem in smaller steps. Learn the base case, the call stack, and real examples like factorial and folder trees. What is recursion in JavaScript? A recursive function is a function that calls itself.

JavaScript recursion is when a function calls itself to solve a problem in smaller steps. Learn the base case, the call stack, and real examples like factorial and folder trees.

What is recursion in JavaScript?

A recursive function is a function that calls itself. Each call works on a smaller piece of the problem, until the piece is so small that the answer is obvious. Then the answers are passed back up and combined.

Think of a set of Russian nesting dolls. To find the smallest doll, you open the outer doll. Inside is another doll, so you do the same thing again: open it. You repeat until you find a doll that does not open. That last doll is where you stop.

Every recursive function needs two parts:

  • A base case: the condition where the function stops and returns an answer without calling itself. This is the doll that does not open.
  • A recursive case: the part where the function calls itself with a smaller or simpler input, moving closer to the base case.

Here is the simplest possible example, a countdown:

function countdown(n) {
  if (n === 0) {              // base case: stop here
    console.log("Liftoff!");
    return;
  }
  console.log(n);
  countdown(n - 1);           // recursive case: same job, smaller number
}

countdown(3);
3
2
1
Liftoff!

countdown(3) prints 3 and calls countdown(2), which prints 2 and calls countdown(1), and so on. When n reaches 0, the base case prints "Liftoff!" and returns without another call, so the chain ends.

The base case and the call stack

To understand recursion, it helps to know about the call stack. The call stack is JavaScript's to-do list of functions that are currently running. Each time a function is called, a new "frame" is put on top of the stack. When a function returns, its frame is taken off, and JavaScript continues with the one below. It works like a stack of plates: you always add and remove at the top.

Let's look at factorial, a classic maths example. The factorial of 4, written 4!, is 4 × 3 × 2 × 1 = 24. Notice that 4! is just 4 × 3!, and 3! is 3 × 2!. That "same problem, but smaller" shape is perfect for recursion:

function factorial(n) {
  if (n <= 1) {        // base case: 1! and 0! are both 1
    return 1;
  }
  return n * factorial(n - 1);   // recursive case
}

console.log(factorial(4));   // Output: 24
console.log(factorial(6));   // Output: 720

Here is what the call stack looks like for factorial(4). First the calls pile up, then the answers come back down:

Going in (each call waits for the one above it):

  factorial(1)  -> base case, returns 1
  factorial(2)  -> waiting: 2 * factorial(1)
  factorial(3)  -> waiting: 3 * factorial(2)
  factorial(4)  -> waiting: 4 * factorial(3)
  ---------------- bottom of the stack

Coming back out:

  factorial(1) returns 1
  factorial(2) returns 2 * 1 = 2
  factorial(3) returns 3 * 2 = 6
  factorial(4) returns 4 * 6 = 24

Each call has its own separate n, because each call gets its own frame. Nothing is multiplied until the base case is reached; then each waiting call finishes its multiplication and returns.

Common mistake: Forgetting the base case, or writing one that is never reached. For example, factorial(n + 1) moves away from 1 instead of towards it. The function then calls itself forever, until JavaScript stops it with an error.

Walking nested data: a folder tree example

Countdowns and factorials are easier with a loop. Recursion really shines with nested data: data where an item can contain more items of the same kind, to any depth. Folders inside folders, comments with replies to replies, and menus with sub-menus all work this way. A loop alone cannot handle them, because you do not know in advance how deep the nesting goes.

const projects = {
  name: "Projects",
  files: [{ name: "notes.txt", size: 12 }],
  folders: [
    {
      name: "website",
      files: [
        { name: "index.html", size: 8 },
        { name: "style.css", size: 4 },
      ],
      folders: [
        { name: "images", files: [{ name: "logo.png", size: 120 }], folders: [] },
      ],
    },
    { name: "empty", files: [], folders: [] },
  ],
};

function totalSize(folder) {
  let size = 0;
  for (const file of folder.files) {
    size += file.size;               // add the files in this folder
  }
  for (const sub of folder.folders) {
    size += totalSize(sub);          // ask each sub-folder for its own total
  }
  return size;
}

console.log(`Total: ${totalSize(projects)} KB`);   // Output: Total: 144 KB

totalSize adds up the files in one folder, then asks each sub-folder for its total by calling itself. The base case is hidden but real: a folder with no sub-folders, like images or empty, never enters the second loop, so it does not call itself again.

We can also print the folder tree, using a depth parameter to indent each level:

function printTree(folder, depth = 0) {
  const indent = "  ".repeat(depth);   // two spaces per level
  console.log(`${indent}${folder.name}/`);
  for (const file of folder.files) {
    console.log(`${indent}  ${file.name}`);
  }
  for (const sub of folder.folders) {
    printTree(sub, depth + 1);       // one level deeper
  }
}

printTree(projects);
Projects/
  notes.txt
  website/
    index.html
    style.css
    images/
      logo.png
  empty/

Building a nested menu with recursion

The same idea builds real web pages. Here a recursive function turns nested menu data into nested <ul> lists, however deep they go:

<!DOCTYPE html>
<html lang="en">
  <body>
    <nav id="site-nav"></nav>

    <script>
      const menu = [
        { label: "Home" },
        {
          label: "Shop",
          children: [
            { label: "Clothes", children: [{ label: "T-shirts" }, { label: "Jackets" }] },
            { label: "Shoes" },
          ],
        },
        { label: "Contact" },
      ];

      function buildMenu(items) {
        const ul = document.createElement("ul");
        for (const item of items) {
          const li = document.createElement("li");
          li.textContent = item.label;
          if (item.children) {
            li.append(buildMenu(item.children));  // a list inside the list item
          }
          ul.append(li);
        }
        return ul;
      }

      document.querySelector("#site-nav").append(buildMenu(menu));
      console.log(document.querySelectorAll("#site-nav li").length);
    </script>
  </body>
</html>
7

On the page you see a bulleted list: Home, Shop and Contact, with Clothes and Shoes indented under Shop, and T-shirts and Jackets indented under Clothes. If you add a deeper level to the data later, the function handles it with no changes.

Recursion vs loops, and stack overflow

Anything you can do with recursion, you can also do with a loop (sometimes with extra work). Use whichever makes the code clearer:

Use recursion whenUse a loop when
The data is nested to an unknown depth (trees, folders, menus, JSON)You go through a flat list or a range of numbers
The problem naturally splits into smaller copies of itselfYou simply repeat a step a known number of times
The recursive version is much shorter and clearerPerformance matters and the input can be very large

Every waiting call takes up space on the call stack, and the stack has a size limit. If a function calls itself too many times (because the base case is missing, or the input is huge), the stack fills up. This is called a stack overflow, and JavaScript stops your code with an error:

function forever(n) {
  return forever(n + 1);   // no base case!
}

try {
  forever(1);
} catch (error) {
  console.log(error.name, "-", error.message);
}
// Output: RangeError - Maximum call stack size exceeded

The exact limit depends on the browser, but it is usually around ten thousand calls. Normal nested data (a few levels of menus or folders) is nowhere near that. For counting up to a million, use a loop.

Tip: When writing a recursive function, write the base case first. Ask yourself: "What is the smallest input, where I already know the answer?" Then ask: "How can I make the input one step smaller?"

Practice exercise

Write a recursive function countComments(comments) for a discussion thread. Each comment is an object like { text: "Great post!", replies: [] }, and replies holds more comments of the same shape. The function should return the total number of comments, including all replies at every depth. Test it with a thread of 2 top-level comments, where one has 2 replies and one of those replies has 1 reply of its own (the answer is 5).

Hint: start with let count = 0;, then for each comment add 1 for the comment itself plus countComments(comment.replies) for its replies. An empty replies array is your base case.

Key takeaways

  • A recursive function calls itself on a smaller version of the problem.
  • Every recursive function needs a base case that stops it, and each call must move closer to that base case.
  • The call stack keeps a frame for every waiting call; answers are combined on the way back out.
  • Recursion is the natural tool for nested data like folder trees, menus and comment threads.
  • Too many nested calls cause a stack overflow (RangeError: Maximum call stack size exceeded); use a loop for long, flat repetition.