How to Use Recursion in JavaScript: Full Tutorial

Recursion is a function that calls itself. Learn base cases, the call stack, practical examples like factorial and tree traversal, and when recursion beats iteration.

6 min read

Recursion is a function that calls itself. Each call works on a smaller or simpler version of the problem until it reaches a stopping condition called the base case. When the base case is met, the function stops calling itself and the results unwind back up.

Here is the simplest recursive function:

javascriptjavascript
function countdown(n) {
  if (n <= 0) {
    console.log("Done!");
    return;
  }
  console.log(n);
  countdown(n - 1);
}

Calling it with a starting number prints each step on the way down to zero, one call at a time, until the base case ends the chain:

javascriptjavascript
countdown(3);
// 3
// 2
// 1
// Done!

countdown calls itself with n - 1 each time. When n reaches 0, the base case fires and the recursion stops.

The Two Essential Parts of Recursion

Every recursive function needs exactly two things:

  • Base case: the condition that stops the recursion. Think of it as the "simplest possible input" where you know the answer without recursing further.
  • Recursive case: the function calls itself with a modified argument that moves closer to the base case.
javascriptjavascript
function factorial(n) {
  // Base case: 0! = 1, 1! = 1
  if (n <= 1) return 1;
 
  // Recursive case: n! = n * (n-1)!
  return n * factorial(n - 1);
}
 
console.log(factorial(5)); // 120

The base case is n <= 1. The recursive case reduces n by 1 each call. Without the base case, factorial would call itself forever.

How the Call Stack Works During Recursion

Each function call adds a frame to the call stack. When a recursive function calls itself, the previous call pauses and waits for the inner call to return:

Recursive call stack for factorial(3)

Each call stacks on top of the previous one. When the base case is reached, the stack unwinds from the top down, multiplying the results as it goes.

Practical Example: Summing an Array

javascriptjavascript
function sum(arr) {
  if (arr.length === 0) return 0;           // Base case: empty array
  return arr[0] + sum(arr.slice(1));         // Recursive case: first + rest
}
 
console.log(sum([1, 2, 3, 4, 5])); // 15

The recursive case takes the first element and adds it to the recursive sum of the rest. Each call passes a smaller array until the array is empty.

Practical Example: Tree Traversal

Recursion is the natural way to traverse nested structures, since a tree is made of smaller trees and a function that handles one node can call itself on each child:

javascriptjavascript
const fileSystem = {
  name: "root",
  children: [
    { name: "src", children: [{ name: "index.js" }, { name: "utils.js" }] },
    { name: "public", children: [{ name: "index.html" }, { name: "style.css" }] },
  ],
};

The function that walks this structure prints a node, then recursively calls itself on every child in the node's children array:

javascriptjavascript
function printTree(node, indent = 0) {
  console.log(" ".repeat(indent) + node.name);
 
  if (node.children) {
    for (const child of node.children) {
      printTree(child, indent + 2);
    }
  }
}

Calling printTree on the root produces an indented listing where deeper files sit further to the right, since the indent grows by two spaces with every level of recursion:

javascriptjavascript
printTree(fileSystem);
// root
//   src
//     index.js
//     utils.js
//   public
//     index.html
//     style.css

Each node prints itself, then recursively prints its children. The base case is a node with no children (a leaf). The recursive case handles nodes with a children array.

Practical Example: Flattening a Nested Array

A nested array is just another tree, so the same recursive shape applies: check each item, and if it is itself an array, recurse into it instead of just copying it over:

javascriptjavascript
function flatten(arr) {
  let result = [];
 
  for (const item of arr) {
    if (Array.isArray(item)) {
      result = result.concat(flatten(item)); // Recursive case
    } else {
      result.push(item);                       // Base case: not an array
    }
  }
 
  return result;
}

Passing in an array with several levels of nesting produces one flat list regardless of how deep the original nesting went:

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

When the function encounters a nested array, it recursively flattens that array and concatenates the result. When it encounters a non-array value, it adds it directly.

Recursion vs Iteration

Any recursive function can be written as a loop, and vice versa. The choice is about clarity and safety, not capability:

RecursionIteration
Best forNaturally nested or divide-and-conquer problemsLinear problems with a known count
Stack usageOne frame per call, risk of overflowConstant, no extra stack frames
ReadabilityOften clearer for tree-like dataOften clearer for simple counting
javascriptjavascript
// Recursive
function factorialRec(n) {
  if (n <= 1) return 1;
  return n * factorialRec(n - 1);
}
 
// Iterative
function factorialIter(n) {
  let result = 1;
  for (let i = 2; i <= n; i++) {
    result *= i;
  }
  return result;
}

When to use recursion:

  • The data is naturally nested (trees, file systems, JSON).
  • The problem divides cleanly into smaller identical subproblems.
  • The recursive solution is clearer and more readable than the loop equivalent.

When to use iteration:

  • The problem is linear and the loop count is known.
  • The recursion depth could be very large.
  • Performance or memory is critical and tail call optimization is not available.

For how to avoid recursion pitfalls, see Preventing Stack Overflow in JavaScript Recursion. For closures, which power many recursive patterns, see JavaScript Closures Deep Dive: Complete Guide.

Rune AI

Rune AI

Key Insights

  • Recursion is a function calling itself with modified arguments until a base case stops it.
  • Every recursive function needs a base case. Without one, the call stack overflows.
  • Recursion shines for tree traversal, nested data, and problems that naturally divide into subproblems.
  • Each recursive call adds a frame to the call stack. Deep recursion can cause stack overflow.
  • Any recursive function can be rewritten as a loop. Choose the form that is clearer for your problem.
RunePowered by Rune AI

Frequently Asked Questions

When should I use recursion over a loop?

Use recursion when the problem naturally breaks into smaller identical subproblems: tree traversal, nested data structures, mathematical sequences like factorial, and divide-and-conquer algorithms. Use a loop when the iteration count is known and the logic is linear.

Does recursion always use more memory than a loop?

Usually yes. Each recursive call adds a frame to the call stack. Deep recursion can cause a stack overflow. But for problems with logarithmic depth (like binary search), the memory overhead is negligible. Some languages optimize tail recursion; JavaScript does not universally.

Can arrow functions be recursive?

Yes, but since arrow functions are anonymous, you must assign them to a variable and call that variable recursively. Be careful: if the variable is reassigned, the recursion breaks. Named function expressions are safer for recursion.

Conclusion

Recursion is a function that calls itself with a smaller problem until it reaches a base case. It is elegant for nested data structures, mathematical sequences, and divide-and-conquer algorithms. The key skill is identifying the base case -- the condition that stops the recursion. Without it, the function calls itself forever until the stack overflows.