Learn recursion in JavaScript. Understand base cases, recursive calls, the call stack, and patterns like factorial, tree traversal, and memoization.
How do you solve a problem by breaking it into smaller versions of the same problem? What if a function could call itself to chip away at a task until it’s done?
Copy
Ask AI
function countdown(n) { if (n === 0) { console.log("Done!") return } console.log(n) countdown(n - 1) // The function calls itself!}countdown(3)// 3// 2// 1// Done!
This is recursion. The countdown function calls itself with a smaller number each time until it reaches zero. It’s a powerful technique that lets you solve complex problems by breaking them into simpler, self-similar pieces.
What you’ll learn in this guide:
What recursion is and its two essential parts (base case and recursive case)
How recursion relates to the call stack
Classic recursive algorithms: factorial, Fibonacci, sum
Optimization techniques: memoization and tail recursion
Prerequisite: This guide assumes you understand JavaScript functions. It also helps to know how the call stack works, though we’ll cover that relationship here.
Recursion is a programming technique where a function calls itself to solve a problem. Instead of using loops, the function breaks a problem into smaller versions of the same problem until it reaches a case simple enough to solve directly. The ECMAScript specification allows functions to reference themselves within their own body, which is the mechanism that makes recursion possible in JavaScript.
Recursion isn’t unique to JavaScript. It’s a fundamental computer science concept found in virtually every programming language. The patterns you learn here apply whether you’re writing Python, C++, Java, or any other language.
Every recursive function has two essential parts:
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ THE TWO PARTS OF RECURSION │├─────────────────────────────────────────────────────────────────────────┤│ ││ function solve(problem) { ││ ││ if (problem is simple enough) { ← BASE CASE ││ return solution directly Stops the recursion ││ } ││ ││ return solve(smaller problem) ← RECURSIVE CASE ││ } Calls itself with simpler input││ │└─────────────────────────────────────────────────────────────────────────┘
Base Case: The condition that stops the recursion. Without it, the function would call itself forever.
Recursive Case: The part where the function calls itself with a simpler or smaller version of the problem.
Here’s a simple example that sums numbers from 1 to n:
Copy
Ask AI
function sumTo(n) { // Base case: when n is 1 or less, return n if (n <= 1) { return n } // Recursive case: n plus the sum of everything below it return n + sumTo(n - 1)}console.log(sumTo(5)) // 15 (5 + 4 + 3 + 2 + 1)console.log(sumTo(1)) // 1console.log(sumTo(0)) // 0
The function asks: “What’s the sum from 1 to 5?” It answers: “5 plus the sum from 1 to 4.” Then it asks the same question with 4, then 3, then 2, until it reaches 1, which it knows is just 1.
Think of recursion like opening a set of Russian nesting dolls (matryoshka). Each doll contains a smaller version of itself, and you keep opening them until you reach the smallest one that can’t be opened.
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ THE RUSSIAN DOLLS ANALOGY │├─────────────────────────────────────────────────────────────────────────┤│ ││ Opening the dolls (making recursive calls): ││ ││ ╔═══════════════════════════════╗ ││ ║ ║ ││ ║ ╔═══════════════════════╗ ║ ││ ║ ║ ║ ║ ││ ║ ║ ╔═══════════════╗ ║ ║ ││ ║ ║ ║ ║ ║ ║ ││ ║ ║ ║ ╔═══════╗ ║ ║ ║ ││ ║ ║ ║ ║ ◆ ║ ║ ║ ║ ← Smallest doll (BASE CASE) ││ ║ ║ ║ ╚═══════╝ ║ ║ ║ Can't open further ││ ║ ║ ║ ║ ║ ║ ││ ║ ║ ╚═══════════════╝ ║ ║ ││ ║ ║ ║ ║ ││ ║ ╚═══════════════════════╝ ║ ││ ║ ║ ││ ╚═══════════════════════════════╝ ││ ││ Each doll = a function call ││ Opening a doll = making a recursive call ││ Smallest doll = base case (stop recursing) ││ Closing dolls back up = returning values back up the chain ││ │└─────────────────────────────────────────────────────────────────────────┘
When you find the smallest doll, you start closing them back up. In recursion, once you hit the base case, the return values bubble back up through each function call until you get your final answer.
To understand recursion, you need to understand how the call stack works. Every time a function is called, JavaScript creates an execution context and pushes it onto the call stack. When the function returns, its context is popped off. According to MDN’s documentation on call stacks, most browsers have a stack size limit — typically around 10,000–25,000 frames — exceeding it throws a RangeError: Maximum call stack size exceeded.With recursion, multiple execution contexts for the same function stack up:
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ THE CALL STACK DURING RECURSION │├─────────────────────────────────────────────────────────────────────────┤│ ││ sumTo(3) calls sumTo(2) calls sumTo(1) ││ ││ STACK GROWING: STACK SHRINKING: ││ ─────────────── ───────────────── ││ ││ ┌───────────────┐ ┌───────────────┐ ││ │ sumTo(1) │ ← current │ │ (popped) ││ │ returns 1 │ └───────────────┘ ││ ├───────────────┤ ┌───────────────┐ ││ │ sumTo(2) │ │ sumTo(2) │ ← current ││ │ waiting... │ │ returns 2+1=3│ ││ ├───────────────┤ ├───────────────┤ ││ │ sumTo(3) │ │ sumTo(3) │ ││ │ waiting... │ │ waiting... │ ││ └───────────────┘ └───────────────┘ ││ ││ Each call waits for the one above it to return ││ │└─────────────────────────────────────────────────────────────────────────┘
Let’s trace through sumTo(3) step by step:
1
sumTo(3) is called
n is 3, not 1, so we need to calculate 3 + sumTo(2). But we can’t add yet because we don’t know what sumTo(2) returns. This call waits.
2
sumTo(2) is called
n is 2, not 1, so we need 2 + sumTo(1). This call also waits.
3
sumTo(1) is called — base case!
n is 1, so we return 1 immediately. No more recursive calls.
4
sumTo(2) resumes
Now it knows sumTo(1) returned 1, so it calculates 2 + 1 = 3 and returns 3.
5
sumTo(3) resumes
Now it knows sumTo(2) returned 3, so it calculates 3 + 3 = 6 and returns 6.
Copy
Ask AI
function sumTo(n) { console.log(`Called sumTo(${n})`) if (n === 1) { console.log(` Base case! Returning 1`) return 1 } const result = n + sumTo(n - 1) console.log(` sumTo(${n}) returning ${result}`) return result}sumTo(3)// Called sumTo(3)// Called sumTo(2)// Called sumTo(1)// Base case! Returning 1// sumTo(2) returning 3// sumTo(3) returning 6
Key insight: Each recursive call creates its own copy of the function’s local variables. The n in sumTo(3) is separate from the n in sumTo(2). They don’t interfere with each other.
Here are the most common recursive algorithms you’ll encounter. Understanding these patterns will help you recognize when recursion is the right tool.
The examples below assume valid, non-negative integer inputs. In production code, you’d want to validate inputs and handle edge cases like negative numbers or non-integers.
Factorial (n!)
The factorial of a number n (written as n!) is the product of all positive integers from 1 to n:
5! = 5 × 4 × 3 × 2 × 1 = 120
3! = 3 × 2 × 1 = 6
1! = 1
0! = 1 (by definition)
The recursive insight: n! = n × (n-1)!
Copy
Ask AI
function factorial(n) { // Base case: 0! and 1! both equal 1 if (n <= 1) { return 1 } // Recursive case: n! = n × (n-1)! return n * factorial(n - 1)}console.log(factorial(5)) // 120console.log(factorial(0)) // 1console.log(factorial(1)) // 1
The Fibonacci sequence starts with 0 and 1, and each subsequent number is the sum of the two before it:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...The recursive definition:
fib(0) = 0
fib(1) = 1
fib(n) = fib(n-1) + fib(n-2) for n > 1
Copy
Ask AI
function fibonacci(n) { // Base cases if (n === 0) return 0 if (n === 1) return 1 // Recursive case: sum of two preceding numbers return fibonacci(n - 1) + fibonacci(n - 2)}console.log(fibonacci(0)) // 0console.log(fibonacci(1)) // 1console.log(fibonacci(6)) // 8console.log(fibonacci(10)) // 55
Performance trap! This naive implementation is very slow for large numbers. fibonacci(40) makes over 300 million function calls because it recalculates the same values repeatedly. We’ll fix this with memoization later.
function sumTo(n) { if (n <= 1) return n return n + sumTo(n - 1)}console.log(sumTo(5)) // 15 (1+2+3+4+5)console.log(sumTo(100)) // 5050console.log(sumTo(0)) // 0
Note: There’s a mathematical formula for this: n * (n + 1) / 2, which is O(1) instead of O(n). For simple sums, the formula is better. But the recursive approach teaches the pattern.
Power Function (x^n)
Calculate x raised to the power of n:
Copy
Ask AI
function power(x, n) { // Base case: anything to the power of 0 is 1 if (n === 0) return 1 // Recursive case: x^n = x * x^(n-1) return x * power(x, n - 1)}console.log(power(2, 0)) // 1console.log(power(2, 3)) // 8console.log(power(2, 10)) // 1024console.log(power(3, 4)) // 81
Optimized version using the property that x^n = (x^(n/2))^2:
Copy
Ask AI
function powerFast(x, n) { if (n === 0) return 1 if (n % 2 === 0) { // Even exponent: x^n = (x^(n/2))^2 const half = powerFast(x, n / 2) return half * half } else { // Odd exponent: x^n = x * x^(n-1) return x * powerFast(x, n - 1) }}console.log(powerFast(2, 10)) // 1024 (but faster!)
The optimized version runs in O(log n) time instead of O(n).
Reverse a String
Reverse a string character by character:
Copy
Ask AI
function reverse(str) { // Base case: empty string or single character if (str.length <= 1) { return str } // Recursive case: last char + reverse of the rest return str[str.length - 1] + reverse(str.slice(0, -1))}console.log(reverse("hello")) // "olleh"console.log(reverse("a")) // "a"console.log(reverse("")) // ""
Recursion really shines when working with nested or tree-like structures. These data structures are naturally recursive, and recursion is often the most elegant solution.
function walkDOM(node, callback) { // Process this node callback(node) // Recurse into child nodes for (const child of node.children) { walkDOM(child, callback) }}// Example: log all tag nameswalkDOM(document.body, (node) => { console.log(node.tagName)})
This pattern combines recursion with higher-order functions (the callback). It’s how browser developer tools display the DOM tree and how libraries traverse HTML structures.
A linked list is a classic recursive data structure where each node points to the next:
Copy
Ask AI
const list = { value: 1, next: { value: 2, next: { value: 3, next: null } }}// Sum all values in the listfunction sumList(node) { if (node === null) return 0 return node.value + sumList(node.next)}console.log(sumList(list)) // 6// Print list in reverse orderfunction printReverse(node) { if (node === null) return printReverse(node.next) // First, go to the end console.log(node.value) // Then print on the way back}printReverse(list)// 3// 2// 1
Performance-critical code: Tight loops in hot paths
Very deep structures: Anything that might exceed ~10,000 levels
Memory-constrained environments: Each recursive call uses stack space
Rule of thumb: Start with whichever approach feels more natural for the problem. If you run into stack overflow issues or performance problems, consider converting to iteration.
Without a base case, the function calls itself forever until the stack overflows:
Copy
Ask AI
// ❌ WRONG - No base case!function countdown(n) { console.log(n) countdown(n - 1) // Never stops!}countdown(3) // 3, 2, 1, 0, -1, -2... CRASH!// RangeError: Maximum call stack size exceeded
Copy
Ask AI
// ✓ CORRECT - Has a base casefunction countdown(n) { if (n < 0) return // Base case: stop at negative console.log(n) countdown(n - 1)}countdown(3) // 3, 2, 1, 0 (then stops)
The error you’ll see:RangeError: Maximum call stack size exceeded. This means you’ve made too many recursive calls without returning. Check your base case!
Even with a base case, if your logic never reaches it, you’ll still crash:
Copy
Ask AI
// ❌ WRONG - Base case can never be reachedfunction countdown(n) { if (n === 0) return // Only stops at exactly 0 console.log(n) countdown(n - 2) // Skips over 0 when starting with odd number!}countdown(5) // 5, 3, 1, -1, -3... CRASH!
Copy
Ask AI
// ✓ CORRECT - Base case is reachablefunction countdown(n) { if (n <= 0) return // Stops at 0 or below console.log(n) countdown(n - 2)}countdown(5) // 5, 3, 1 (then stops)
Memoization means caching the results of function calls so you don’t recompute the same thing twice. It’s especially useful for recursive functions with overlapping subproblems.
Copy
Ask AI
// Fibonacci with memoizationfunction fibonacci(n, memo = {}) { // Check if we already calculated this if (n in memo) { return memo[n] } // Base cases if (n <= 1) return n // Calculate and cache the result memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo) return memo[n]}console.log(fibonacci(50)) // 12586269025 (instant!)console.log(fibonacci(100)) // 354224848179262000000 (still instant!)
The naive Fibonacci has O(2^n) time complexity. With memoization, it’s O(n). That’s the difference between billions of operations and just 100.
A tail recursive function is one where the recursive call is the very last thing the function does. There’s no computation after the call returns.
Copy
Ask AI
// NOT tail recursive - multiplication happens AFTER the recursive callfunction factorial(n) { if (n <= 1) return 1 return n * factorial(n - 1) // Still need to multiply after call returns}// Tail recursive version - uses an accumulatorfunction factorialTail(n, accumulator = 1) { if (n <= 1) return accumulator return factorialTail(n - 1, accumulator * n) // Nothing to do after this returns}
Why does this matter? In theory, tail recursive functions can be optimized by the JavaScript engine to reuse the same stack frame, avoiding stack overflow entirely. This is called Tail Call Optimization (TCO).
Reality check: Most JavaScript engines (V8 in Chrome/Node, SpiderMonkey in Firefox) do not implement TCO. Safari’s JavaScriptCore is the notable exception — it has supported TCO since 2016. So in practice, tail recursion doesn’t prevent stack overflow in most environments. The ECMAScript 2015 specification does define tail call optimization in strict mode, but engine adoption remains limited. Still, it’s good to understand the concept, as it’s important in functional programming languages like Haskell and Scheme.
What are the two essential parts of a recursive function?
Answer:
Base case: The condition that stops the recursion. It returns a value without making another recursive call.
Recursive case: The part where the function calls itself with a simpler or smaller version of the problem.
Copy
Ask AI
function example(n) { if (n === 0) return "done" // Base case return example(n - 1) // Recursive case}
What happens if you forget the base case?
Answer:The function calls itself infinitely until JavaScript throws a RangeError: Maximum call stack size exceeded. This is called a stack overflow because each call adds a frame to the call stack until it runs out of memory.
Copy
Ask AI
// This will crashfunction broken(n) { return broken(n - 1) // No base case to stop!}broken(5) // RangeError: Maximum call stack size exceeded
Write a recursive function to find the length of an array without using .length
Answer:
Copy
Ask AI
function arrayLength(arr) { // Base case: empty array has length 0 if (arr.length === 0) return 0 // Recursive case: 1 + length of the rest return 1 + arrayLength(arr.slice(1))}console.log(arrayLength([1, 2, 3, 4])) // 4console.log(arrayLength([])) // 0
Note: We use .length only to check if the array is empty (our base case). The actual counting happens through recursion, not by directly returning .length.
Why is naive Fibonacci recursion inefficient, and how would you fix it?
Answer:Naive Fibonacci recalculates the same values many times. For example, fib(5) calculates fib(3) twice, fib(2) three times, etc. This leads to exponential O(2^n) time complexity.The fix: Memoization. Cache results so each value is only calculated once:
Copy
Ask AI
function fibonacci(n, memo = {}) { if (n in memo) return memo[n] if (n <= 1) return n memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo) return memo[n]}
This reduces the time complexity to O(n).
When should you choose recursion over a loop?
Answer:Choose recursion when:
Working with tree-like or nested structures (DOM, file systems, JSON)
The problem naturally divides into self-similar subproblems
Code clarity is more important than maximum performance
Implementing divide-and-conquer algorithms
Choose loops when:
Iterating through flat, linear data
Performance is critical
You might recurse more than ~10,000 levels deep
Memory is constrained
How does recursion relate to the call stack?
Answer:Each recursive call creates a new execution context that gets pushed onto the call stack. The function waits for its recursive call to return, keeping its context on the stack.When the base case is reached, contexts start popping off the stack as return values bubble back up. This is why deep recursion can cause stack overflow — too many contexts waiting at once.
Recursion is when a function calls itself to solve a problem by breaking it into smaller instances of the same problem. Every recursive function needs a base case (when to stop) and a recursive case (how to reduce the problem). As MDN’s glossary defines it, recursion is an act of a function calling itself until a termination condition is met.
What happens if I forget the base case?
Without a base case, the function calls itself indefinitely until the JavaScript engine runs out of call stack space and throws a RangeError: Maximum call stack size exceeded. Most browsers limit the stack to around 10,000–25,000 frames. Always ensure your base case is reachable from every possible input.
When should I use recursion instead of loops?
Recursion excels with naturally recursive data structures like trees, nested objects, and linked lists. For simple iteration over arrays or counting, a for loop is typically clearer and more performant. If the problem involves branching paths (like traversing a file system or DOM tree), recursion is usually the more natural solution.
Does JavaScript support tail call optimization?
The ECMAScript 2015 specification defines tail call optimization in strict mode, but most engines have not implemented it. Safari’s JavaScriptCore is the only major engine with TCO support. In practice, use memoization or convert deep recursion to iteration when stack depth is a concern.
How deep can recursion go in JavaScript?
The maximum recursion depth depends on the JavaScript engine and available memory. Chrome’s V8 typically allows around 10,000–15,000 stack frames, while other engines vary. For problems requiring deep recursion, consider converting to an iterative approach using an explicit stack data structure.