Recursion in JavaScript

Write functions that call themselves: base cases, recursive cases, the call stack, walking nested data and trees, and when a loop is the better choice.

Lesson 22 of 56Intermediate16 min read

A recursive function is a function that calls itself. That sounds like a recipe for an endless loop, and without care it is. But used well, recursion is an elegant way to solve problems that contain smaller copies of themselves.

Think about a folder on your computer. It holds files and other folders, and each of those folders holds files and more folders, to any depth. Counting every file with a normal loop is awkward, because you don't know how deep it goes. With recursion it's natural: count the files in this folder, then ask the same function to count each sub-folder. The same shape appears in menus with sub-menus, comment threads with replies, the HTML DOM tree and JSON data from APIs.

In this lesson you'll learn the two parts every recursive function needs, trace how the call stack handles each call, walk nested data, and learn when a simple loop is the better tool. You'll also see the famous "Maximum call stack size exceeded" error and how to avoid it.

The two parts of recursion

JavaScript
function countdown(n) {
  if (n === 0) {            // base case: stop here
    console.log("Liftoff");
    return;
  }
  console.log(n);
  countdown(n - 1);         // recursive case: a smaller problem
}
countdown(3);
  • The base case is the condition where the function stops calling itself. Without it, the function never ends.
  • The recursive case calls the function again with a smaller or simpler input, moving towards the base case.
  • Each call must make progress. countdown(n) calling countdown(n) again would never reach zero.

Returning values

Most recursive functions build their answer from the results of the smaller calls:

JavaScript
function sum(numbers) {
  if (numbers.length === 0) return 0;          // base case
  return numbers[0] + sum(numbers.slice(1));   // first + sum of the rest
}
sum([2, 4, 6]); // 2 + (4 + (6 + 0)) = 12

Read it as a definition: "the sum of a list is its first number plus the sum of the rest, and the sum of an empty list is 0". Many recursive functions are easiest to write by describing the answer this way first.

The call stack

Every function call is placed on the call stack until it returns. With recursion, calls pile up:

Stack (top is the newest)Waiting for
sum([6])sum([])
sum([4, 6])sum([6])
sum([2, 4, 6])sum([4, 6])

When the base case returns 0, the calls finish one by one from the top down. The stack has a size limit, roughly ten thousand calls in most browsers. A missing base case or very deep data fills it, and JavaScript throws RangeError: Maximum call stack size exceeded. You can watch the stack in DevTools, as shown in the debugging lesson.

Recursion vs loops

ProblemBetter toolWhy
Count from 1 to 100LoopSimple, flat, no stack growth
Total an arrayLoop or reduceFlat data
Nested folders, menus, commentsRecursionThe data is a tree of unknown depth
Deep-copy or compare nested objectsRecursionEach level looks like the one above
Very deep chains (100,000 levels)Loop with your own stack arrayAvoids stack overflow

Any recursive function can be rewritten as a loop, and vice versa. Choose whichever is clearer for the shape of the data.

JavaScript Recursion Syntax

Recursive function pattern

function solve(input) {
  if (/* simplest case */) return /* direct answer */;
  return /* combine */ solve(/* smaller input */);
}

function walk(node) {
  console.log(node.name);
  for (const child of node.children ?? []) walk(child);
}

JavaScript Recursion Summary

  • A recursive function calls itself on a smaller version of the problem.
  • Every recursive function needs a base case that stops it.
  • Each call goes on the call stack until it returns.
  • A missing base case causes "Maximum call stack size exceeded".
  • Use recursion for nested, tree-shaped data; use loops for flat data.

JavaScript Recursion Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

Factorial and countdown

function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

console.log("5! =", factorial(5));
console.log("10! =", factorial(10));

function countdown(n) {
  if (n === 0) return console.log("Go!");
  console.log(n);
  countdown(n - 1);
}
countdown(3);
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 2The base case: the factorial of 1 or 0 is 1.
  2. Line 35! is 5 times 4!, and so on down to 1.
  3. Line 10Stop at zero.
  4. Line 12Count down by calling itself with a smaller number.
Example 2

Walking a nested menu

const menu = {
  name: "Shop",
  children: [
    { name: "Clothing", children: [
      { name: "Coats" },
      { name: "Shoes", children: [{ name: "Trainers" }, { name: "Boots" }] },
    ]},
    { name: "Home" },
  ],
};

function printMenu(item, depth = 0) {
  console.log("  ".repeat(depth) + "- " + item.name);
  for (const child of item.children ?? []) {
    printMenu(child, depth + 1);
  }
}

function countItems(item) {
  let total = 1;
  for (const child of item.children ?? []) total += countItems(child);
  return total;
}

printMenu(menu);
console.log("Total items:", countItems(menu));
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 12 to 13Print this item, indented by its depth.
  2. Lines 14 to 16Then print each child one level deeper. Items without children are the base case.
  3. Lines 19 to 22Count this item plus everything below it.
Example 3

Flattening nested arrays

function flatten(list) {
  const out = [];
  for (const item of list) {
    if (Array.isArray(item)) {
      out.push(...flatten(item));
    } else {
      out.push(item);
    }
  }
  return out;
}

console.log(flatten([1, [2, [3, [4]], 5], 6]));
console.log([1, [2, [3, [4]], 5], 6].flat(Infinity));
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 4 to 5If the item is itself an array, flatten it recursively and add its items.
  2. Line 7Plain values are the base case.
  3. Line 14Built-in flat(Infinity) does the same job, but now you know how it works.

Common JavaScript Recursion Mistakes

No base case

Don't
function down(n) {
  console.log(n);
  down(n - 1); // never stops: Maximum call stack size exceeded
}
Do
function down(n) {
  if (n < 0) return;
  console.log(n);
  down(n - 1);
}

Not making the problem smaller

Don't
function sum(arr) {
  if (arr.length === 0) return 0;
  return arr[0] + sum(arr); // same array again
}
Do
function sum(arr) {
  if (arr.length === 0) return 0;
  return arr[0] + sum(arr.slice(1));
}

Forgetting to return the recursive result

Don't
function find(node, name) {
  if (node.name === name) return node;
  for (const c of node.children ?? []) find(c, name); // result is thrown away
}
Do
function find(node, name) {
  if (node.name === name) return node;
  for (const c of node.children ?? []) {
    const hit = find(c, name);
    if (hit) return hit;
  }
  return null;
}

JavaScript Recursion in Real Projects

Recursion in real projects

File explorers and cloud storage apps list folders recursively. Comment systems like Reddit render reply threads with recursive components. JSON.stringify, structuredClone and deep-equality checks in testing libraries walk objects recursively. Browsers walk the DOM tree, and React renders component trees, using the same idea.

JavaScript Recursion Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the base case so power(2, 0) returns 1.

function power(base, exp) {
  if (exp === ) return 1;
  return base * power(base, exp - 1);
}
console.log(power(2, 5));
Predict the outputmedium

What does this log?

function f(n) {
  if (n === 0) return "";
  return n + f(n - 1);
}
console.log(f(3));
Multiple choiceeasy

What stops a recursive function from running forever?

Multiple choicemedium

Which error appears when recursion goes too deep?

Fix the bugmedium

depth(obj) should return how deeply objects are nested: {} is 1, { a: {} } is 2. Non-object values count as 0. Fix the function.

function depth(value) {
  if (typeof value === "object") return 0;
  let deepest = 0;
  for (const key in value) {
    deepest = Math.max(deepest, depth(value));
  }
  return deepest;
}
console.log(depth({ a: { b: {} } }));
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • Empty object is 1
  • Nested objects count each level
  • Plain values are 0
  • Takes the deepest branch

JavaScript Recursion Quiz

5 questions to check what you learned.

  1. What is recursion?

    • A loop inside a loop
    • A function with many arguments
    • Repeating code by copying it
    • A function calling itself
  2. Which data is recursion best suited for?

    • A flat list of prices
    • A single number
    • A tree, such as folders inside folders
    • A short string
  3. What happens to each call while it waits for the next one?

    • It's deleted
    • It stays on the call stack until it can finish
    • It runs in parallel
    • It moves to the heap
  4. Can every recursive function be written as a loop?

    • Yes, possibly with your own stack array
    • No, never
    • Only factorial
    • Only in Python
  5. In return n * fact(n - 1), when is the multiplication done?

    • Before fact(n - 1) is called
    • At the same time
    • After fact(n - 1) returns its result
    • Never

JavaScript Recursion Challenge

Count files in folders

Write countFiles(folder). A folder looks like { files: ["a.txt"], folders: [ ...more folders ] }, and either array may be missing.

  1. Count the files directly in this folder (0 if files is missing)
  2. Add the result of calling countFiles on every sub-folder
  3. Return the total
function countFiles(folder) {

}

const drive = {
  files: ["notes.txt"],
  folders: [
    { files: ["a.jpg", "b.jpg"] },
    { folders: [{ files: ["deep.pdf"] }] },
  ],
};
console.log(countFiles(drive));
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • Counts every level
  • Empty folder is 0
  • Calls itself