memoize: caching a function by its first argument, and the ways that goes wrong
Memoize functions - an optimization technique used to speed up consecutive function calls by caching the result of calls with identical input
At a glance
- What is it?
- memoize is a small TypeScript library that wraps a function and caches its result, and its most important behaviour is also its most surprising one, because by default only the first argument is compared and the comparison is exact, so a second parameter is silently ignored. It suits a synchronous function called repeatedly with the same input, and the README spends most of its length on the cache key options that make it behave the way you meant.
- Who is it for?
- Adopt memoize when you have a synchronous function that is called repeatedly with the same input and the result is expensive to compute, because the wrapper is a few lines and the default behaviour is right for the single argument case.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 50 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- sindresorhus/memoize1,168 ★ · +2
| Repository | 2026-10-19 | 2026-11-07 |
|---|---|---|
| sindresorhus/memoize | 1.2k | 1.2k |
2 Stars in the last 19 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
What the library does
memoize is an optimization used to speed up consecutive function calls by caching the result of calls with identical input, and it is packaged as a tiny TypeScript library with a single runtime dependency.
The mechanism is the familiar one. You wrap a function, and the wrapper looks up the cache before calling through, and stores the result afterwards. Memory is automatically released when an item expires or the cache is cleared.
The package is minimal in a way worth noting. The manifest lists exactly one dependency, mimic-function, which is the small library that copies a function's own properties onto the wrapper so that a memoized function still looks like the original. The engine requirement is Node 22 or later, the package is an ES module with type declarations, and the published files are only the compiled distribution directory.
The API is two functions. The main one takes a function and an optional options object, and the second, memoizeDecorator, returns a decorator for the cases where you would rather annotate than wrap.
The options are three, and each of them exists because of a specific way the default behaviour is wrong. The maxAge option sets the lifetime of an entry, the cacheKey option decides what identifies an entry, and the cache option replaces the storage.
The project is MIT licensed, by Sindre Sorhus, the current release is 11.0.0 from March 2026, and the last push to the repository was on 2026-09-18.
The counter example, and what it quietly hides
The usage example in the README is a counter, and it is chosen to show two things at once, one of which is a trap.
import memoize from 'memoize';
let index = 0;
const counter = () => ++index;
const memoized = memoize(counter);
memoized('foo');
//=> 1
// Cached as it's the same argument
memoized('foo');
//=> 1
// Not cached anymore as the argument changed
memoized('bar');
//=> 2
memoized('bar');
//=> 2The first three calls behave exactly as you would hope. The fourth line is the one to look at.
By default, only the memoized function's first argument is considered, via strict equality comparison. The comment in the README says it, and the example demonstrates it: calling with an extra argument returns the cached value for the first argument alone, so the counter does not advance.
That is not a bug, it is the documented default, and it is the single most likely way to get a wrong answer out of this library. A function whose second argument matters, and a function whose first argument is an object, both produce silently incorrect results, because the cache key does not distinguish the calls.
The next section of the README, the caching strategy section, is where the project deals with exactly this, and it is the part worth reading before you use the library on anything but the simplest function.
cacheKey: making the key match the semantics
The cacheKey option is the escape hatch, and the README gives a worked example of the failure it fixes.
The first example is a power function with two arguments, memoized with no key function at all.
import memoize from 'memoize';
const pow = memoize((a, b) => Math.pow(a, b));
pow(2, 2); // => 4, stored in cache with the key 2 (number)
pow(2, 3); // => 4, retrieved from cache at key 2 (number), it's wrongThe comment in the source is blunt about it, it is wrong. The second call returns 4 instead of 8 because the key is the first argument alone.
The fix is a cacheKey function that considers all the arguments, and the default value of the option is documented as taking the first argument, with an example of serialising the whole argument list.
import memoize from 'memoize';
const pow = memoize((a, b) => Math.pow(a, b), {
cacheKey: arguments_ => arguments_.join(',')
});
pow(2, 2); // => 4, stored in cache with the key '2,2' (both arguments as one string)
pow(2, 3); // => 8, stored in cache with the key '2,3'The key function can return any type the cache supports, so a string key is fine, and the default Map accepts objects as keys as well. The point of the option is that the key is a function of the arguments, so you decide what makes two calls the same call.
Object arguments and the serialisation route
Objects are the case where the default fails most confusingly, because two calls with identical looking arguments are not the same call.
The README's example takes a function that accepts an options object and shows that out of the box it will not be memoised usefully, since the object is stored as the key and a second call with a fresh object literal is a different object even though the two look identical.
import memoize from 'memoize';
const heavyMemoizedOperation = memoize(heavyOperation);
heavyMemoizedOperation({full: true}); // Stored in cache with the object as key
heavyMemoizedOperation({full: true}); // Stored in cache with the object as key, again
// The objects appear the same, but in JavaScript, they're different objectsThe suggested fix is to serialise or hash the arguments, for example with JSON.stringify or with a library that also handles regular expressions, dates and similar values.
import memoize from 'memoize';
const heavyMemoizedOperation = memoize(heavyOperation, {cacheKey: JSON.stringify});
heavyMemoizedOperation({full: true}); // Stored in cache with the key '[{"full":true}]' (string)
heavyMemoizedOperation({full: true}); // Retrieved from cacheThe same approach works when there are several serialisable arguments, in which case the key is the serialised list.
heavyMemoizedOperation('hello', {full: true}); // Stored in cache with the key '["hello",{"full":true}]' (string)
heavyMemoizedOperation('hello', {full: true}); // Retrieved from cacheThe trade in this route is that serialising every argument on every call costs something, and that a serialised key can be much larger than the object it replaces.
The cache option, and not leaking memory
The cache option replaces the storage, and the contract it must satisfy is spelled out in the API section.
The default is a new Map. An alternative must implement the get, set and delete methods, and optionally a clear method. The README suggests a WeakMap instead, or quick-lru for an LRU cache.
The WeakMap case is the interesting one, because it changes the lifetime semantics. A WeakMap holds its keys weakly, so when the key object becomes unreachable the entry can be collected. That is exactly the right behaviour when the key is an object you were going to hold anyway, because the cache then costs nothing extra in memory.
For arguments that WeakMap cannot hold, such as DOM elements and functions, the README points at a different library, many-keys-map, which extends the exact equality comparison to work on multiple arguments.
import memoize from 'memoize';
import ManyKeysMap from 'many-keys-map';
const addListener = (emitter, eventName, listener) => emitter.on(eventName, listener);
const addOneListener = memoize(addListener, {
cacheKey: arguments_ => arguments_, // Use *all* the arguments as key
cache: new ManyKeysMap() // Correctly handles all the arguments for exact equality
});And the README then says something better: if the arguments are compatible with a WeakMap, use a weak map implementation instead, because it will help avoid memory leaks.
So there is a preference order, and it is worth restating it. If the arguments can be weak keys, use a weak map. If they cannot but are serialisable, serialise them into a string key. If they are neither, use the multi key map and accept that the entries live as long as the cache does.
maxAge, and the promise case
The maxAge option sets the lifetime of a cache entry in milliseconds, and it accepts either a number or a function.
The semantics are given as three cases. A value of zero or negative means the result is not cached at all, so the function always runs. Infinity means the entry is cached indefinitely with no expiration. A positive finite number is the number of milliseconds before the entry expires.
When a function is supplied, it receives the arguments and returns the max age, and the documented example computes it from the current time, so a cache entry can expire based on when it was created rather than on a fixed offset.
const memoizedGot = memoize(got, {maxAge: 1000});The example in the README shows the whole cycle with a network request, where the first call is made, the second is served from the cache, and a delay of two seconds later the entry has expired and the request goes out again.
The one thing the README steers you away from is promise returning functions. If you want to memoize an async function, the note says you might be better served by p-memoize, which is the Promise specific sibling, and the async example in the README is there to show that the caching works but that the promise specific behaviours live elsewhere.
That distinction is the real design boundary of this library. It handles the synchronous case and the storage concerns, and it leaves the concurrency concerns, where two calls in flight for the same key should share one promise, to the other package.
The project, and how it is built and tested
The repository is small in the way a well factored library should be. There is a single source file at the root, a test file, a directory of type tests, the package manifest and the readme.
The build is TypeScript compiled with tsc into a distribution directory, and the manifest points the exports at the compiled types and the compiled JavaScript, with a side effects flag set to false so a bundler can tree shake the package if it is unused. The prepack script builds before publishing, so the distribution directory is always fresh.
The test command is a chain of four checks, linting with xo, running the test suite with ava, building, and then running the type level tests with tsd against the built declarations. The type tests matter more than usual in a library like this, because the whole value of the options is in their types, and a type level test is what proves that a cacheKey function returning a string and one returning an object are both accepted.
The ava configuration runs the TypeScript tests as ES modules through a loader, with the worker threads turned off and a one minute timeout, which suggests the suite includes timing sensitive tests around expiry.
The dependency list tells you what the library does not do. There is no cache library, no serialisation library and no date library, so the only thing in the box is the wrapper, the key function and the expiry check, and anything else, the serialiser, the weak map, the LRU cache, is a choice you make and a dependency you add.
Editorial conclusion
Adopt memoize when you have a synchronous function that is called repeatedly with the same input and the result is expensive to compute, because the wrapper is a few lines and the default behaviour is right for the single argument case. Do not adopt it for a function that returns a promise, because the README points at p-memoize for that, and do not adopt it for a multi argument function without reading the caching strategy section, because the default key ignores every argument but the first and will return a wrong result. Before you ship it, check the key function against the arguments that actually change the answer.
Frequently asked questions
What does the memoize package do?
It wraps a function and caches the result of calls with identical input, speeding up consecutive calls. Memory is released automatically when an item expires or the cache is cleared, and the package has a single runtime dependency, mimic-function, which copies the original function's properties onto the wrapper.
How does memoize decide two calls are the same?
By default only the first argument is considered, compared with strict equality, and the README is explicit that this is wrong for a function whose other arguments matter. You supply a cacheKey function to fix it, for example joining the arguments into a string or serialising them with JSON.stringify.
How do I stop a memoize cache from growing without bound?
Use the maxAge option, which takes milliseconds or a function of the arguments, where zero or negative means do not cache, Infinity means cache indefinitely, and a positive number is the lifetime. You can also replace the storage with the cache option, using a WeakMap or an LRU cache, as long as it implements get, set, delete and optionally clear.
Should I use memoize for async functions?
The README suggests p-memoize for functions that return promises, and says you might be better served by it for Promise specific behaviors. The async example in the readme shows that the caching itself works, but the promise handling lives in the sibling package.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sindresorhus-memoize)Star history chart for your README
[](https://hysenlabs.com/projects/sindresorhus-memoize)Was this useful?