What Is Recursion?

Recursion is when a function calls itself as part of its own definition. That sounds circular — because it is, intentionally. The key is that each recursive call works on a smaller version of the same problem, until it reaches a case simple enough to answer directly.

Every recursive solution has two essential parts:

  1. Base case — the condition where the function stops calling itself and returns a direct result.
  2. Recursive case — the call to itself with a smaller/simpler input.

Without a base case, you get infinite recursion and a stack overflow. This is why identifying your stopping condition is the first step, always.

The Mental Model: Think in Smaller Problems

The trick to writing recursive functions is to trust the recursion. Instead of mentally simulating every call, assume your function already works correctly for smaller inputs — then just define what to do for the current input in terms of those smaller ones.

For example, to calculate the factorial of n:

  • Factorial of 0 = 1 (base case — just know this)
  • Factorial of n = n × factorial(n - 1) (recursive case — trust that factorial(n-1) works)
function factorial(n) {
  if (n === 0) return 1;        // Base case
  return n * factorial(n - 1); // Recursive case
}

console.log(factorial(5)); // 120

Visualizing the Call Stack

When factorial(3) runs, here's what the call stack looks like:

factorial(3)
  → 3 * factorial(2)
         → 2 * factorial(1)
                → 1 * factorial(0)
                       → 1   ← base case returns
               ← 1 * 1 = 1
        ← 2 * 1 = 2
  ← 3 * 2 = 6

Each call waits for the one below it to return. The stack unwinds from the bottom up — the deepest call resolves first.

A More Practical Example: Traversing Nested Objects

Recursion is the natural fit for problems with unknown or variable depth — like traversing a nested comment tree or a file system structure.

const tree = {
  value: 1,
  children: [
    { value: 2, children: [] },
    { value: 3, children: [
      { value: 4, children: [] }
    ]}
  ]
};

function sumTree(node) {
  if (node.children.length === 0) return node.value; // Base case: leaf node
  const childSum = node.children.reduce((acc, child) => acc + sumTree(child), 0);
  return node.value + childSum;
}

console.log(sumTree(tree)); // 10

Try writing this iteratively — it quickly gets messy. Recursion handles the variable depth naturally.

Recursion vs. Iteration: When to Use Which

ScenarioBetter Choice
Simple loops with known countIteration
Tree or graph traversalRecursion
Divide-and-conquer algorithms (merge sort, binary search)Recursion
Performance-critical, flat dataIteration
Parsing nested structures (JSON, HTML, ASTs)Recursion

Watch Out for Stack Overflow

Each recursive call adds a frame to the call stack. Very deep recursion (thousands of calls) can hit JavaScript's stack limit. Solutions include:

  • Tail call optimization — rewrite so the recursive call is the final operation (limited JS engine support)
  • Memoization — cache results of subproblems to avoid redundant calls
  • Convert to iteration with an explicit stack array if depth is unbounded

Key Takeaways

  • Always define your base case first — it's your exit condition.
  • Trust that your function works for n-1 when defining the n case.
  • Recursion is the natural choice for tree-shaped or divide-and-conquer problems.
  • Be mindful of stack depth for large inputs.

Recursion becomes intuitive once you stop trying to simulate every call in your head. Define the base, trust the rest, and let the call stack do its job.