Memoization: caching results without caching your bugs
Memoization stores a function's results by input. Learn cache keys, eviction, memoizing async functions safely, and when caching makes things worse.
Comes up in: Coding, DSA
Learn#
Suresh’s dashboard shows a user’s name in five widgets. Each widget calls getUser(7), so every page load sends five identical requests. He adds a cache, and now a single network error is stuck on screen until the page reloads.
Memoization stores a function’s results by its inputs, so a repeat call returns the stored answer.
The cache key must capture everything that changes the result. Joining arguments works for simple values; objects need a custom key, such as an ID.
function memoize(fn, key = (...args) => args.join("|")) {
const cache = new Map();
return (...args) => {
const k = key(...args);
if (!cache.has(k)) cache.set(k, fn(...args));
return cache.get(k);
};
}
let calls = 0;
const square = memoize((n) => { calls++; return n * n; });
console.log(square(9), square(9), square(4), "calls:", calls);
const fib = memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
console.log(fib(50));Output (verified)
81 81 16 calls: 2 12586269025
Without the cache, fib(50) makes billions of calls; with it, about fifty.
For async functions, cache the promise, not the result — then calls made while the first is still running share it. And remove the entry if the promise rejects, so one failure is not remembered forever.
Cache
- 7 → promise (pending)
Network
- GET /users/7
Three components ask for user 7 at the same time. The first call starts the fetch and caches its promise.
Read the steps as text
- Three components ask for user 7 at the same time. The first call starts the fetch and caches its promise.
- Calls two and three find the same promise in the cache. No new requests.
- The request fails. The promise rejects, so it is removed from the cache.
- The next call starts a fresh request, instead of getting the old failure forever.
Predict#
Predict first
memoize(fetchUser) caches results only after the await finishes. Five widgets call it at the same moment. How many requests are sent?
The cache is empty until the first request finishes, so all five calls miss it and start their own request. Caching the promise immediately fixes this: the first call stores the pending promise, and the others reuse it.
Interview angle#
Say it like this — about 30 seconds
“Memoization caches a function’s results by a key built from its arguments, so repeat calls skip the work. It is safe for pure functions. The risks are wrong keys, unbounded memory growth, and stale data. For async functions I cache the promise immediately, so concurrent callers share one request, and I delete the entry if it rejects, so errors are retried. I add a size limit or time-to-live, and I memoize only when the work is measurably expensive.”
Follow-up How would you memoize an async function safely?
Follow-up How do you stop a memo cache growing forever?
Follow-up When does memoization hurt?
Practice#
On your own
The starter caches only after await, and would cache nothing useful on failure. Rewrite memoizeAsync to cache the promise and forget failures.
Done when
- Three calls in flight with the same key make one call to
fn; all get the same value. - A later call with that key does not call
fnagain. - After a rejection, the next call calls
fnagain; different keys are cached separately.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
What is memoization?
Caching a function's results by its inputs so repeat calls skip the work.
Why cache the promise rather than the result for async functions?
So calls made while the first is running share it instead of starting their own.
What should happen to a cached promise that rejects?
Remove it from the cache so the next call retries.
Name two risks of memoization.
Unbounded memory growth and stale or wrongly keyed results.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Same input, same output, no side effects — then memoization is safe. Choose a key that captures every input that matters.
Check yourself: Should you memoize a function that returns the current time?
No. It is not pure; the same input does not give the same output.
How it works underneathSenior rounds
A memoized function is a closure over its cache (JS.02.03). For recursion, the inner calls must use the memoized name — fib above calls the wrapped fib — so every subproblem is computed once. This is top-down dynamic programming (DSA.12) Source: MDN: Memoization.
Check yourself: Why does fib only work when it calls the memoized version of itself?
The recursive calls must go through the cache, or they recompute everything.
Edge cases and trapsWhere people slip
args.join("|") turns every object into “[object Object]”, so different objects collide; use a resolver like (user) => user.id. JSON.stringify keys depend on property order. Eviction — removing old entries — needs a policy: least recently used, time-to-live, or both.
Check yourself: Why does args.join('|') fail for objects?
Every plain object becomes '[object Object]', so different objects share one key.
In productionStaff-level answers
For network data, prefer a data library such as TanStack Query or SWR: they de-duplicate in-flight requests, cache with expiry, retry failures and refresh in the background Source: TanStack Query: Overview. In React, useMemo and memo are memoization too — measure before adding them.
Check yourself: Which libraries handle request de-duplication and caching for React apps?
TanStack Query or SWR.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- What is memoization? What are its risks?
- How would you memoize an async function safely?