On this page
Memoize
Last reviewed 22 Sept 2026
Problem
Implement memoize(fn, resolver?). The returned function caches results by argument, so repeated calls with the same input skip the work:
const slowSquare = (n) => { for (let i = 0; i < 1e7; i++); return n * n; };const fast = memoize(slowSquare);fast(9); // slow the first timefast(9); // instant — from the cachefast.cache.clear();Clarifying questions
- How should multiple arguments form a key? Default: the first argument only (lodash) or all arguments serialised — agree on one. A custom
resolvercovers the rest. - Object arguments: compare by reference or by content? By reference is safest; content keys (
JSON.stringify) are slower and ambiguous. - Should the cache be limited in size? A good follow-up: LRU.
- Async functions: cache the promise or the value? The promise — so concurrent calls share one request.
Approach
Keep a Map in the closure. Compute a key from the arguments; if the key is in the map, return the stored result; otherwise call fn, store the result and return it. Exposing the map as memoized.cache lets callers clear or inspect it.
Step-by-step build
Step 1 — single argument
function memoize(fn) { const cache = new Map(); return function (arg) { if (cache.has(arg)) return cache.get(arg); // has(), not get(): results may be undefined const result = fn.call(this, arg); cache.set(arg, result); return result; };}Step 2 — many arguments and a resolver
function memoize(fn, resolver = (...args) => JSON.stringify(args)) { const cache = new Map(); function memoized(...args) { const key = resolver.apply(this, args); if (cache.has(key)) return cache.get(key); const result = fn.apply(this, args); cache.set(key, result); return result; } memoized.cache = cache; return memoized;}Step 3 — async functions
Caching the promise means two calls made at the same time share one request. Remove rejected promises so a failure is not cached forever.
const result = fn.apply(this, args);cache.set(key, result);if (result && typeof result.then === 'function') { result.catch(() => cache.delete(key));}Step 4 — limit the size (LRU)
A Map keeps insertion order: re-inserting a key on each hit moves it to the end, so the first key is the least recently used.
Final code
function memoize(fn, { resolver = (...args) => JSON.stringify(args), maxSize = Infinity } = {}) { const cache = new Map();
function memoized(...args) { const key = resolver.apply(this, args); if (cache.has(key)) { const hit = cache.get(key); cache.delete(key); // refresh recency cache.set(key, hit); return hit; } const result = fn.apply(this, args); cache.set(key, result); if (cache.size > maxSize) cache.delete(cache.keys().next().value); // evict least recently used if (result && typeof result.then === 'function') { result.then(undefined, () => cache.delete(key)); // do not cache failures } return result; }
memoized.cache = cache; return memoized;}Edge cases
- A function that returns
undefinedis still cached —has()distinguishes “cached undefined” from “missing”. JSON.stringifykeys:f({ a: 1, b: 2 })andf({ b: 2, a: 1 })get different keys, and functions orundefinedinside arguments disappear from the key. Offer a resolver for object arguments.- Memoizing an impure function (depends on time, randomness or outside state) gives stale results — only memoize pure functions.
- Rejected promises are removed so the next call retries.
Follow-ups
- Reference keys for objects: nest
WeakMaps per argument so object keys are compared by identity and garbage-collected. - Time-to-live: store
[value, expiresAt]and treat expired entries as missing. - Memoized Fibonacci: recursion through the memoized function turns O(2ⁿ) into O(n).
- React:
useMemo,React.memoand why they only cache the last input, not every input.
Common mistakes
if (cache[key])— misses cached falsy results (0,'',false).- Using a plain object as the cache: keys become strings and inherit prototype properties like
constructor. - An unbounded cache in a long-running server — a memory leak.
- Losing
thiswhen memoizing methods.