JavaScript
Recursion
Memoization
Programming
Performance Optimization

memoize any given recursive function in JavaScript

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Memoization is a good fit for recursive functions that solve the same subproblem many times. The important detail in JavaScript is that a recursive function must call the memoized version of itself, not the original version, or the cache will be bypassed.

Why naive wrapping is not enough

If you take an existing recursive function and pass it through a generic memoize helper, only the outermost call is guaranteed to use the cache. Inner recursive calls may still go back to the original function name, which means repeated work still happens.

That is why a reusable solution for recursive code usually uses open recursion. Instead of a function calling its own name directly, it receives a recur function and uses that for the next step. The memoizer can then intercept every recursive call.

A memoizer for recursive functions

The helper below works for many pure recursive functions. It stores results in a Map, and it lets you customize the cache key when the default JSON.stringify behavior is not ideal.

javascript
1function memoizeRecursive(fn, keyFn = (...args) => JSON.stringify(args)) {
2  const cache = new Map();
3
4  function recur(...args) {
5    const key = keyFn(...args);
6
7    if (cache.has(key)) {
8      return cache.get(key);
9    }
10
11    const value = fn(recur, ...args);
12    cache.set(key, value);
13    return value;
14  }
15
16  return recur;
17}
18
19const fib = memoizeRecursive((recur, n) => {
20  if (n < 2) {
21    return n;
22  }
23
24  return recur(n - 1) + recur(n - 2);
25});
26
27console.log(fib(10));
28console.log(fib(40));

In this pattern, fn never calls its own name. It always goes through recur, which is the cached entry point. That single design choice is what makes the helper work for arbitrary recursive logic rather than only for top-level calls.

Applying it to more than Fibonacci

A useful test is a function with multiple parameters. Counting paths in a grid is a classic example. From any cell, you can move right or down, and many branches reach the same coordinate repeatedly.

javascript
1function memoizeRecursive(fn, keyFn = (...args) => JSON.stringify(args)) {
2  const cache = new Map();
3
4  function recur(...args) {
5    const key = keyFn(...args);
6    if (cache.has(key)) {
7      return cache.get(key);
8    }
9
10    const value = fn(recur, ...args);
11    cache.set(key, value);
12    return value;
13  }
14
15  return recur;
16}
17
18const countPaths = memoizeRecursive((recur, row, col) => {
19  if (row === 0 || col === 0) {
20    return 1;
21  }
22
23  return recur(row - 1, col) + recur(row, col - 1);
24});
25
26console.log(countPaths(3, 3));
27console.log(countPaths(8, 8));

This version is still recursive and easy to read, but it avoids recomputing the same coordinates over and over. Without caching, the call tree grows explosively. With caching, each unique pair is solved once.

Choosing good cache keys

The default JSON.stringify(args) approach is acceptable for numbers, strings, booleans, and plain arrays. It becomes less reliable when arguments include functions, class instances, or objects whose property order is not stable.

For those cases, provide a custom key function. For example, if your recursion depends only on two integers, a key such as ${row}:${col} is more direct and cheaper to compute. The goal is simple: equivalent inputs must always produce the same key.

Memoization also works best when the function is pure. If the result depends on time, random values, global state, or network responses, a cached answer may be wrong on the next call.

Common Pitfalls

  • Wrapping a recursive function after the fact and expecting inner calls to be cached. If the function calls its own original name, the memoizer only helps at the boundary.
  • Using unstable cache keys for object arguments. Two logically identical inputs can miss the cache if the keying strategy is inconsistent.
  • Memoizing impure logic. Caching is only safe when the same inputs should always return the same result.
  • Letting the cache grow forever. For long-running processes, consider a size limit or a strategy for clearing entries when they are no longer useful.
  • Forgetting that async recursion needs different handling. If the function returns a Promise, cache the promise itself and handle rejection carefully.

Summary

  • Generic memoization for recursion works best when the recursive function receives a recur callback.
  • Every recursive step must go through the memoized entry point or the cache will be skipped.
  • 'Map is a straightforward cache store, but key design matters for correctness.'
  • Memoization is most effective for pure functions with overlapping subproblems.
  • It improves speed by trading memory for fewer repeated computations.

Course illustration
Course illustration

All Rights Reserved.