Toolmingo
Guides11 min read

Recursion Explained: What It Is, How It Works, and When to Use It

Master recursion — base cases, recursive cases, call stack, memoization, tail recursion, and 10 classic problems with working code in Python, JavaScript, Java, and Go.

Recursion is when a function calls itself to solve a smaller version of the same problem. Instead of writing a loop, you trust the function to handle the next step — as long as you define exactly when it should stop.

Every recursive solution has two parts:

  1. Base case — the condition that stops the recursion (no more self-calls)
  2. Recursive case — the self-call that moves closer to the base case

Miss the base case and you get infinite recursion. Miss the recursive case and nothing happens. Get both right and you unlock an elegant problem-solving pattern.


Quick reference

Concept What it means
Base case When the function stops calling itself
Recursive case The self-call with a smaller input
Call stack Memory that tracks every active function call
Stack overflow Too many nested calls exhaust the call stack
Memoization Caching results to avoid recomputing the same subproblem
Tail recursion Self-call is the very last operation (some compilers optimise it)
Direct recursion Function calls itself
Indirect / mutual recursion Function A calls B, which calls A

How the call stack works

Every function call reserves a stack frame — a slot in memory that holds local variables, parameters, and the return address. When a function calls itself recursively, a new frame is pushed on top of the stack. When it returns, that frame is popped.

Here is what the call stack looks like when you compute factorial(4):

factorial(4)            ← pushed first
  factorial(3)          ← pushed
    factorial(2)        ← pushed
      factorial(1)      ← base case: returns 1
    ← returns 1 * 1 = 1
  ← returns 2 * 1 = 2
← returns 3 * 2 = 6
← returns 4 * 6 = 24

Each indentation level is one stack frame. Most runtimes cap the stack at a few thousand frames (Python defaults to 1,000). Exceeding that limit raises a stack overflow error.


Your first recursive function: factorial

Factorial of n (written n!) is n × (n−1) × (n−2) × … × 1. The base case is factorial(0) = 1.

Python

def factorial(n: int) -> int:
    # base case
    if n == 0:
        return 1
    # recursive case
    return n * factorial(n - 1)

print(factorial(5))   # 120
print(factorial(0))   # 1

JavaScript

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

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

Java

public static long factorial(int n) {
    if (n == 0) return 1;              // base case
    return n * factorial(n - 1);      // recursive case
}

System.out.println(factorial(5));  // 120

Go

func factorial(n int) int {
    if n == 0 { return 1 }          // base case
    return n * factorial(n-1)       // recursive case
}

fmt.Println(factorial(5))  // 120

Classic example 2: Fibonacci

The Fibonacci sequence is 0, 1, 1, 2, 3, 5, 8, 13… Each number is the sum of the two before it.

def fib(n: int) -> int:
    if n <= 1:           # base cases: fib(0)=0, fib(1)=1
        return n
    return fib(n - 1) + fib(n - 2)   # recursive case

print(fib(10))   # 55

Warning: naive Fibonacci is exponentially slow — fib(50) makes over a trillion calls. Fix it with memoization (below).


Memoization: caching recursive results

Memoization stores the result of each unique call so you never solve the same subproblem twice. It turns the naïve Fibonacci from O(2ⁿ) to O(n).

Python (built-in cache)

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n: int) -> int:
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(50))   # 12586269025  (instant)

JavaScript (manual cache)

const memo = {};

function fib(n) {
  if (n <= 1) return n;
  if (memo[n] !== undefined) return memo[n];
  memo[n] = fib(n - 1) + fib(n - 2);
  return memo[n];
}

console.log(fib(50));  // 12586269025

The memoized call graph now looks like a straight line instead of a tree — each fib(k) is computed exactly once.


Recursion vs iteration

Both solve the same problems. Choose based on clarity and constraints.

Recursion Iteration (loop)
Code clarity Often cleaner for tree/graph/divide-and-conquer problems Often cleaner for simple repetition
Memory Uses call-stack space — O(depth) Usually O(1) extra memory
Stack overflow risk Yes, on very deep inputs No
Tail-call optimisation Some compilers eliminate frames N/A
Best for Trees, graphs, backtracking, divide-and-conquer Counting, accumulation, simple loops
Debugging Harder — stack trace is long Easier — single function
Performance Function-call overhead per step Lower overhead

Rule of thumb: if the problem naturally decomposes into smaller identical subproblems (trees, graphs, permutations, divide-and-conquer), recursion wins on clarity. If it is a simple counter or accumulator, use a loop.


Converting recursion to iteration

Every recursive algorithm can be converted to an iterative one using an explicit stack. Here is factorial as a loop:

def factorial_iter(n: int) -> int:
    result = 1
    while n > 1:
        result *= n
        n -= 1
    return result

And Fibonacci with a loop (O(1) space):

def fib_iter(n: int) -> int:
    if n <= 1:
        return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

Tail recursion

A function is tail recursive when the recursive call is the last operation — no work happens after it returns.

# NOT tail recursive — multiplies after the recursive call returns
def factorial(n):
    if n == 0: return 1
    return n * factorial(n - 1)   # multiplication happens on the way back up

# Tail recursive — accumulator carries the result
def factorial_tail(n, acc=1):
    if n == 0: return acc
    return factorial_tail(n - 1, n * acc)   # call is the last thing

Languages like Scheme, Erlang, Elixir, and some Scala compilers can optimise tail-recursive calls into a loop (eliminating stack growth). Python and Java do NOT do this, so tail recursion in those languages still risks stack overflow on large inputs.


Divide and conquer: merge sort

Merge sort is a textbook recursion pattern: split the array in half, sort each half recursively, then merge.

def merge_sort(arr: list) -> list:
    if len(arr) <= 1:                   # base case
        return arr
    mid = len(arr) // 2
    left  = merge_sort(arr[:mid])       # recursive case
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(left: list, right: list) -> list:
    result, i, j = [], 0, 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i]); i += 1
        else:
            result.append(right[j]); j += 1
    return result + left[i:] + right[j:]

print(merge_sort([5, 2, 8, 1, 9, 3]))  # [1, 2, 3, 5, 8, 9]

Time: O(n log n) in all cases. Space: O(n) for the temporary arrays.


Tree traversal

Trees are the most natural data structure for recursion — every node is the root of its own subtree.

class Node:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

def inorder(node):           # Left → Root → Right  (sorted for BST)
    if node is None: return
    inorder(node.left)
    print(node.val, end=" ")
    inorder(node.right)

def preorder(node):          # Root → Left → Right
    if node is None: return
    print(node.val, end=" ")
    preorder(node.left)
    preorder(node.right)

def postorder(node):         # Left → Right → Root  (delete bottom-up)
    if node is None: return
    postorder(node.left)
    postorder(node.right)
    print(node.val, end=" ")
Traversal Order Common use
Inorder Left → Root → Right Print BST in sorted order
Preorder Root → Left → Right Serialise / clone a tree
Postorder Left → Right → Root Delete tree, evaluate expression trees
Level-order (BFS) Level by level Shortest path in unweighted trees

Backtracking

Backtracking is recursive exploration with the ability to undo a choice when it leads to a dead end. The pattern:

  1. Make a choice
  2. Recurse
  3. Undo the choice (backtrack)

All permutations of a list:

def permutations(nums: list) -> list[list]:
    result = []

    def backtrack(current, remaining):
        if not remaining:           # base case — permutation complete
            result.append(current[:])
            return
        for i in range(len(remaining)):
            current.append(remaining[i])
            backtrack(current, remaining[:i] + remaining[i+1:])
            current.pop()           # undo choice

    backtrack([], nums)
    return result

print(permutations([1, 2, 3]))
# [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

Classic backtracking problems: N-Queens, Sudoku solver, generating all subsets, word search on a grid.


10 classic recursive problems

Problem Key insight Complexity
Factorial n! = n × (n-1)! O(n) time, O(n) stack
Fibonacci (memoised) fib(n) = fib(n-1) + fib(n-2), cache results O(n) time, O(n) space
Binary search Halve the search space each call O(log n)
Merge sort Split, sort halves, merge O(n log n)
Quick sort Partition around pivot, recurse each side O(n log n) avg, O(n²) worst
Tree height 1 + max(height(left), height(right)) O(n)
Sum of nested list If element is a list, recurse; else add O(n)
Power (fast) x^n = x^(n/2) × x^(n/2) (if even) O(log n)
Tower of Hanoi Move n-1 discs to aux, move largest, move n-1 back O(2ⁿ)
All subsets Include or exclude each element, recurse O(2ⁿ)

Mutual (indirect) recursion

Two functions can call each other — common when parsing grammars or checking even/odd:

def is_even(n: int) -> bool:
    if n == 0: return True
    return is_odd(n - 1)

def is_odd(n: int) -> bool:
    if n == 0: return False
    return is_even(n - 1)

print(is_even(4))   # True
print(is_odd(7))    # True

Recursion depth limits

Language Default limit How to change
Python 1,000 sys.setrecursionlimit(10_000)
JavaScript (V8) ~10,000–15,000 No standard API — increase stack size at process level
Java ~500–1,000 (JVM stack size) -Xss JVM flag (e.g. -Xss8m)
Go 1 GB goroutine stack (grows dynamically) Rarely an issue
Rust ~128 threads-stack size (configurable) RUST_MIN_STACK env var

For deep recursion, prefer iteration or an explicit stack over changing runtime limits.


Common mistakes

Mistake What happens Fix
Missing base case Infinite recursion → stack overflow Always define when to stop
Base case never reached Recursion goes in wrong direction Ensure each call makes input smaller
Wrong return statement Returns None / undefined instead of value Return the recursive call's result
Modifying shared state Incorrect results in backtracking Copy state, or undo mutations after recursion
Naive exponential recursion Correct but too slow Add memoisation or switch to DP
Relying on tail-call optimisation Stack overflow in Python/Java Convert to iterative for large n
Off-by-one in base case Wrong output for edge cases Test with n=0, n=1, empty input
Passing mutable default arg State persists between calls (Python) Use None default, create inside function

Recursion vs dynamic programming

Dynamic programming (DP) is memoised recursion written bottom-up. Both solve overlapping subproblems; the choice is style:

Recursion + memo (top-down) DP table (bottom-up)
Code style Natural — follows the recurrence Requires figuring out order
Space Call stack + memo table Memo table only
Stack overflow risk Yes No
When to prefer Complex recurrences, sparse subproblems Large n, performance-critical

FAQ

Q: Why does Python limit recursion to 1,000 calls?
A: CPython does not optimise tail calls, and deeply recursive programs can exhaust the C stack. The limit protects against accidental infinite recursion crashing the interpreter. You can raise it with sys.setrecursionlimit(), but for large depths prefer an iterative solution.

Q: Is recursion slower than iteration?
A: Slightly — each function call has overhead (push/pop a stack frame, create local variables). For simple problems like summing an array, iteration is faster. For tree traversal or divide-and-conquer, the difference is negligible and recursion is clearer.

Q: What is the difference between recursion and a loop?
A: A loop explicitly repeats a block of code using a counter or condition. Recursion re-invokes a function with smaller input and relies on the call stack to track state. Any recursion can be converted to a loop (using an explicit stack if needed), and vice versa.

Q: When should I use recursion?
A: Use recursion when the problem has a naturally recursive structure — trees, graphs, divide-and-conquer (merge sort, binary search), combinatorics (permutations, subsets), and backtracking (Sudoku, N-Queens). Avoid it for simple sequential loops where iteration is clearer and avoids stack overhead.

Q: What causes a "maximum recursion depth exceeded" / stack overflow?
A: Either a missing base case (infinite recursion) or legitimate deep recursion that exceeds the stack limit. Debug by printing function arguments on each call to see if the problem shrinks toward the base case.

Q: How does memoisation differ from dynamic programming?
A: Memoisation (top-down DP) is recursion with a cache — you start from the top and store results as you go. Bottom-up DP fills a table starting from the smallest subproblems. Both have the same time complexity; memoisation is often easier to write, bottom-up uses less stack space.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools