Skip to content
JavaScript js functions 4 min read

Recursion

Recursion is a technique where a function solves a problem by calling itself on a smaller version of that problem. It shines when data is naturally nested or self-similar — trees, file systems, deeply nested arrays — where a loop would be awkward. The trick to writing correct recursion is disciplined: every recursive function needs a base case that stops the recursion and a recursive case that moves toward it. Get either wrong and you get infinite recursion and a crash.

Base case and recursive case

Think of recursion as answering a question by assuming you already know the answer to a slightly smaller question. The base case is the smallest input you can answer directly without recursing. The recursive case reduces the problem and calls the function again, trusting that smaller call to return the right result.

The classic example is the factorial: n! = n * (n-1)!, where 0! = 1.

function factorial(n) {
  if (n <= 1) return 1; // base case
  return n * factorial(n - 1); // recursive case
}

console.log(factorial(5));

Output:

120

Each call multiplies n by the result of the next-smaller call, until n reaches the base case and the chain unwinds: 5 * 4 * 3 * 2 * 1.

Tip: Always write the base case first. If you find yourself unsure when to stop, the function probably isn’t ready to be recursive yet.

How the call stack works

Every function call pushes a frame onto the call stack — a record of its arguments and where to resume when it returns. With recursion, frames stack up until the base case is hit, then they pop off in reverse order. Visualising the stack for factorial(3) makes this concrete.

factorial(3)            -> 3 * factorial(2)
  factorial(2)          -> 2 * factorial(1)
    factorial(1)        -> 1            (base case, returns)
  factorial(2)          -> 2 * 1 = 2
factorial(3)            -> 3 * 2 = 6

The stack has finite size. Recurse too deeply and you exhaust it, which brings us to the main risk.

Stack overflow and how to avoid it

If the base case is never reached — or the input is simply too large — the stack runs out of space and the engine throws a RangeError.

function countDown(n) {
  console.log(n);
  countDown(n - 1); // bug: no base case
}

// countDown(100000); // RangeError: Maximum call stack size exceeded

JavaScript engines do not reliably optimise tail calls (despite the ES2015 spec allowing it), so you cannot count on deep recursion being free. For large or unbounded inputs, convert to iteration, which uses a fixed amount of stack:

function factorialIterative(n) {
  let result = 1;
  for (let i = 2; i <= n; i++) result *= i;
  return result;
}

console.log(factorialIterative(5));

Output:

120

The table below summarises the trade-offs.

AspectRecursiveIterative
Readability for nested dataExcellentOften clumsy
MemoryOne stack frame per callConstant (or explicit stack)
Risk of stack overflowYes, on deep inputNo
Best fitTrees, graphs, divide-and-conquerFlat sequences, hot loops

Traversing a tree

Recursion is the natural fit for tree-shaped data, because each node is itself a small tree. Here we sum every value in a binary tree by recursing into both children.

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

function sumTree(node) {
  if (!node) return 0; // base case
  return node.value + node.children.reduce((acc, child) => acc + sumTree(child), 0);
}

console.log(sumTree(tree));

Output:

10

The recursive case delegates each child to sumTree and adds up the results — no manual stack bookkeeping required.

Flattening a nested array

Another everyday use is flattening arbitrarily nested arrays. The base case is a non-array value; the recursive case digs into nested arrays.

function flatten(arr) {
  return arr.reduce((flat, item) => {
    return flat.concat(Array.isArray(item) ? flatten(item) : item);
  }, []);
}

console.log(flatten([1, [2, [3, [4]], 5]]));

Output:

[ 1, 2, 3, 4, 5 ]

For most real code, the built-in Array.prototype.flat(Infinity) does this job — but writing it by hand is the clearest way to internalise how recursion descends through structure.

An iterative alternative with an explicit stack

When you need recursion’s behaviour but can’t risk overflowing the call stack, simulate it with an explicit array used as a stack. This keeps the depth-first logic while bounding memory to the heap.

function sumTreeIterative(root) {
  let total = 0;
  const stack = [root];
  while (stack.length > 0) {
    const node = stack.pop();
    if (!node) continue;
    total += node.value;
    stack.push(...node.children);
  }
  return total;
}

console.log(sumTreeIterative(tree));

Output:

10

Warning: Recursion that depends on user-supplied depth (parsing untrusted JSON, walking a directory the user picked) can be a denial-of-service vector. Prefer an explicit-stack version or impose a depth limit.

Best Practices

  • Write the base case first, and make sure every recursive path provably reaches it.
  • Ensure each recursive call works on a strictly smaller input so the problem shrinks.
  • Reserve recursion for naturally nested or self-similar data; use loops for flat sequences.
  • Don’t rely on tail-call optimisation — it isn’t dependable across JavaScript engines.
  • For deep or untrusted input, switch to an iterative version with an explicit stack.
  • Memoise overlapping subproblems (e.g. naive Fibonacci) to avoid exponential blow-up.
  • Keep recursive functions pure where possible; passing accumulators as parameters is cleaner than mutating outer state.
Last updated June 1, 2026
Was this helpful?