DEV Community

Figsy
Figsy

Posted on

Beware of "lodash.memoize": It Only Remembers the First Argument

memoize looks like free performance – wrap a function, get caching, move on. But lodash's version has a sharp edge: by default it builds the cache key from the first argument only. Every other argument is ignored.

One argument? You're fine. Two arguments? You're one stale result away from a bug that sails through code review.

The trap in five lines

import memoize from 'lodash/memoize';

const add = memoize((a, b) => a + b);

add(1, 2); // 3  — computed, cached under key `1`
add(1, 9); // 3  — WRONG, should be 10
Enter fullscreen mode Exit fullscreen mode

The second call should be 10. lodash saw 1 again, found it in the cache, and returned the old result without ever looking at 9.

A version that actually ships

Currency formatting is the classic real-world trap — same amount, different currency:

const formatPrice = memoize((amount, currency) =>
  new Intl.NumberFormat('en', { style: 'currency', currency }).format(amount)
);

formatPrice(100, 'USD'); // "$100.00"  — cached under key `100`
formatPrice(100, 'EUR'); // "$100.00"  — should be "€100.00"!
Enter fullscreen mode Exit fullscreen mode

The second call ignores 'EUR' entirely. The key is just 100, already seen, so you get dollars where you wanted euros. No error, no warning — just quietly wrong money on the screen.

The fix: write a resolver

memoize takes an optional second argument that returns the cache key. Make it cover every argument that affects the result:

const formatPrice = memoize(
  (amount, currency) =>
    new Intl.NumberFormat('en', { style: 'currency', currency }).format(amount),
  (amount, currency) => `${amount}|${currency}` // key reflects both
);

formatPrice(100, 'USD'); // "$100.00"
formatPrice(100, 'EUR'); // "€100.00"  ✅
Enter fullscreen mode Exit fullscreen mode

The rule: the key must uniquely capture everything that can change the output — no more, no less. Miss one input and you've recreated the bug in a sneakier form.

And sometimes: don't memoize at all

That formatter is microseconds of work. Caching it adds a permanent cache and a correctness risk to save almost nothing. Memoization earns its keep only when the function is genuinely expensive and called repeatedly with the same inputs. For cheap functions, the simplest correct code is to just call them.

Takeaways

  • Default memoize keys on the first argument only.
  • More than one argument → write a resolver that encodes all of them.
  • A good key reflects everything that changes the output.
  • Cheap function? Often the right move is no memoize at all. memoize is a fine tool – it just trusts you to define what "the same call" means, and its silent default is rarely the answer past one argument.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev

That lodash.memoize behavior is a perfect example of an API footgun hiding behind a friendly name. Memoization sounds generic, but the cache key contract is the whole feature. If the key is wrong, the optimization becomes a bug.