completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Memoization & laziness

Learn to memoize pure functions, choose safe cache keys, cap memory, cache async promises, and build lazy values and sequences.

By the end, you can
  • 01
    Write a safe memoizeSave answers in a Map, and count calls to show which ones were skipped.
  • 02
    Pick the right cache keyChoose between a plain key, a JSON.stringify key, nested maps, a WeakMap, a saved promise, or a size limit.
  • 03
    Wait to do work until it is neededUse thunks, getters that replace themselves, ??=, generators, and iterator helpers to work out only what is asked for.

Save work, delay work

Some work is slow, like a long calculation or a request to a server. This lesson shows two ways to do less of it:

  • Save the answer. If you already worked something out, keep the answer and use it again.
  • Wait until it is needed. Don't work something out until your code asks for it.
Definition

Memoization means saving the result of a function call. The next time the function gets the same inputs, it gives back the saved result instead of doing the work again. The place where saved results are kept is called a cache.

Lazy evaluation means waiting to do work until the value is needed. Sometimes only part of a long list ever gets worked out.

This lesson uses ideas from earlier lessons: pure functions, closures, Map & Set, WeakMap & WeakSet, generators, and iterator helpers. Each one is explained again when it shows up.

Real-life analogyA library help desk

A librarian at the help desk hears the same questions again and again. When she finds a hard answer, she writes it on a card. The next time someone asks, she reads the card. She also doesn't take every book off the shelves before anyone asks. She fetches a book only when someone wants it.

In real life: The librarian writes a hard answer on a card
In JavaScript: A memoized function saves a result in the cache
In real life: The same question gets the card, not a new search
In JavaScript: The saved result comes back, and the slow work is skipped
In real life: Books stay on the shelf until someone asks
In JavaScript: Lazy code works out a value only when it is asked for

Build memoize with a Map

STEP THROUGH

Let's build a helper called memoize. You give it a function. It gives back a new function, called a wrapper. The wrapper does the same job, but it saves its answers.

The wrapper keeps its answers in a Map. A Map stores pairs: a key and a value. Here the key is the argument, and the value is the answer for that argument.

Each time you call the wrapper, one of two things happens:

  • A cache hit: the key is already in the Map. The wrapper returns the saved answer. The original function does not run.
  • A cache miss: the key is not there yet. The wrapper calls the original function, saves the answer under the key, and returns it.

The Map is made once, inside memoize. The wrapper is a closure: a function that still remembers the variables around it. So the same Map stays alive from one call to the next.

The example uses Fibonacci numbers. Each one is the sum of the two before it: 0, 1, 1, 2, 3, 5, 8. So fib(6) is 8. A plain recursive fib works out the same smaller numbers again and again.

To show the saving, the code counts how often the real function body runs, in a variable called calls. A count is the same on every computer, so it is better proof than a stopwatch.

  • Plain recursion: fib(6) runs the body 25 times.
  • Memoized: fib(6) runs the body 7 times, once for each key from 0 to 6.
  • A second memoized fib(6) runs the body 0 more times, because it is a cache hit.
The first fib(6) is a miss. The body runs and saves its answers in the cache, including 5: 5 and 6: 8. calls is 7. The second fib(6) is a hit. It reads 8 from the cache, the body is skipped, and calls is still 7.1st fib(6)8miss: body runscalls: 7saves5: 56: 8cachereads2nd fib(6)8hit: body skippedcalls: 7
The first fib(6) runs the body and saves every answer from 0 to 6. The second fib(6) reads 8 from the cache, so calls stays at 7.
Memoized Fibonacci: hits and misses
Step 0 of 49Ready
Your turn: follow the blue line

Step through a memoized recursive function. Watch calls: it shows how many times the real body ran, and cache hits don't add to it.

Running in
  1. script
Next: line 1
  const cache = new Map();  return function memoized(arg) {    if (cache.has(arg)) return cache.get(arg);    const result = fn(arg);    cache.set(arg, result);    return result;  };} let calls = 0;let fib;fib = memoize((n) => {  calls++;  return n < 2 ? n : fib(n - 1) + fib(n - 2);}); console.log(fib(4));console.log(calls);console.log(fib(4));console.log(calls);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the Fibonacci number

Picking a number starts the replay again. Watch calls: cache hits don't add to it.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Pure functions only

Memoization is only safe for a pure function. A pure function's answer depends only on its arguments, and it changes nothing outside itself. A price formatter or a math calculation is pure.

Don't memoize a function that does any of these, unless you also clear the cache when things change:

  • It reads the time or a random number.
  • It reads the page (the DOM) or a global variable.
  • It has a side effect: it changes something outside itself, such as a variable or a server.
Clear the cache when a hidden input changesPop out in the code editor (opens in a new tab)JavaScript
function memoize(fn) {
  const cache = new Map();
  const memoized = (key) => {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
  memoized.clear = () => cache.clear();
  return memoized;
}

let taxRate = 0.1;
const total = memoize((price) => Math.round(price * (1 + taxRate)));
console.log(total(100));
taxRate = 0.2;
console.log(total(100));
total.clear();
console.log(total(100));

The total function reads taxRate, but taxRate is not one of its arguments. That makes it a hidden input.

  1. Line 15 prints 110: 100 plus 10% tax.
  2. Line 16 changes the tax to 20%.
  3. Line 17 still prints 110. The key is still 100, so it is a cache hit, and the old answer comes back.
  4. Line 18 empties the cache with the clear method that line 9 added.
  5. Line 19 is now a miss. It works out the total again and prints 120.

In a real app, clear the cache whenever the data behind it changes.

Memoizing a side effect skips itPop out in the code editor (opens in a new tab)JavaScript
function memoize(fn) {
  const cache = new Map();
  return (key) => {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
}

let visits = 0;
const labelVisit = memoize((name) => {
  visits++;
  return name + ":" + visits;
});

console.log(labelVisit("Asha"));
console.log(labelVisit("Asha"));
console.log(visits);

This function adds 1 to visits each time it runs. That is a side effect. Line 17 runs it and prints Asha:1. Line 18 is a cache hit, so the function does not run. It prints Asha:1 again, and visits stays at 1. The second visit was never counted, so don't memoize a function like this.

Choose what counts as the same input

CHANGE INPUT

The key decides when two calls count as "the same input". So pick it on purpose:

  • One simple argument, such as a string or a number: use the argument itself as the key.
  • Several arguments: joining them into one string can mix them up. Put one Map inside another instead. These are called nested maps.
  • An object argument: decide if you mean "this exact object" or "any object with the same fields".

Each row of this table is one kind of input. Read across to see which key to use and what to watch out for.

Which cache key to use for each kind of input
What the function getsKey to useWhat to watch out for
One simple argument, like a string or a number.Use the argument itself as the key in a Map.Nothing much. This is the simplest and clearest case.
Several simple arguments.Use nested Maps, or build a joined key with care.Joined strings can mix up pairs: '1' + '23' and '12' + '3' are both '123'.
A plain object.First decide: "this exact object" or "same fields"?A Map treats two objects with the same fields as two different keys.
An object turned into a string with JSON.stringify.Use it only if the same input always gives the same string.Field order changes the string. Some values are left out, and an object that points to itself throws an error.
Extra data that belongs to one object.Use a WeakMap, with the object as the key.Nothing much. When the rest of the app stops using the object, the cache lets it go too.
Two objects with the same fields are two keysPop out in the code editor (opens in a new tab)JavaScript
const cache = new Map();
const firstUser = { id: 1 };
const secondUser = { id: 1 };
cache.set(firstUser, "saved");

console.log(cache.get(firstUser));
console.log(cache.get(secondUser) ?? "miss");
  1. Line 4 saves the value "saved" under the object firstUser.
  2. Line 6 looks up that same object, so it prints saved.
  3. Line 7 looks up secondUser. It has the same field, but it is a different object. So the lookup gives undefined.
  4. The ?? operator gives its right side when its left side is null or undefined. So line 7 prints miss.

A Map matches object keys by which object they are, not by what is inside them. Only build a key from the fields when your app really treats objects with the same fields as the same thing.

Left: cache.get(firstUser) looks up the object that was saved, so it prints saved. Right: cache.get(secondUser) looks up a different object with the same field, id: 1, so it prints miss.cache.get(firstUser)the saved objectid: 1firstUsersavedcache.get(secondUser)new object, same fieldid: 1secondUsermiss
Both objects hold id: 1, but they are two different objects. Only firstUser was saved, so only its lookup prints saved.
Try four kinds of cache key
One string as the keyPop out in the code editor (opens in a new tab)JavaScript
function memoize(fn) {  const cache = new Map();  return (key) => {    if (cache.has(key)) return cache.get(key);    const result = fn(key);    cache.set(key, result);    return result;  };} let calls = 0;const shout = memoize((name) => {  calls++;  return name.toUpperCase();}); console.log(shout("ada"));console.log(shout("ada"));console.log(calls);
OutputOne string
  1. ADA
  2. ADA
  3. 1
Try it yourself

The key is the string "ada". The second shout("ada") finds the same key in the Map, so the real function runs only once and calls is 1.

Each button runs real Map or WeakMap code. Reset goes back to the one-string key.
Joined strings can mix up two pairsPop out in the code editor (opens in a new tab)JavaScript
const cache = new Map();
const key = (first, second) => first + second;

cache.set(key("1", "23"), "first pair");
console.log(cache.get(key("12", "3")));

Line 2 makes a key by joining two strings. On line 4, "1" and "23" become "123". On line 5, "12" and "3" also become "123". So the lookup finds the wrong entry and prints first pair. Nested maps keep the two arguments apart, so this mix-up can't happen.

JSON.stringify turns an object into a string, so you can use that string as a key. It works only for small, plain objects whose fields are always added in the same order. It has three problems:

  • It leaves out some values, such as functions and undefined.
  • It throws an error if an object points back to itself.
  • It keeps the order the fields were added in. Two objects with the same fields in a different order give different strings.

Nested Maps avoid these problems when "the same object" is what you mean.

WeakMap caches and size limits

MEMORY

A Map keeps every key and value for as long as the Map exists. Say a page stays open for hours and keeps adding keys. Then the cache keeps growing. Memory that fills up and is never freed is called a memory leak.

A WeakMap helps when the keys are objects. When nothing else in your program uses a key object any more, JavaScript may throw away that object and its cached value. Freeing unused memory like this is called garbage collection.

What a WeakMap promises
  • Its keys must be objects.
  • You can't loop over it, and it has no size.
  • An entry may disappear at any time once nothing else uses its key object.

You can't force that cleanup or watch it happen. So write code that works whether the entry is still there or not.

Each row of this table is one place to keep cached answers. Read across to see how it behaves and when to use it.

Four places to keep cached answers
Where you keep answersHow it behavesWhen to use it
A Map.It keeps every entry until you delete it. You can loop over it, and it has size.Everyday memoization, when you decide when to clear it.
Nested Maps.There is one Map level for each argument.Functions with several arguments, without joining them into one string.
A WeakMap.Keys must be objects. You can't loop over it, and it has no size.Extra data for an object that should go away when the object does.
A Map with a size limit.It deletes the oldest, or least recently used, key when it gets too big.Pages that stay open a long time, where a cache could grow without end.

When the keys are not objects, set a size limit instead. The panel below keeps at most two answers. It follows a "least recently used" rule:

  • A hit moves its key to the newest spot.
  • When a third key is added, the oldest key is deleted.

A Map remembers the order its keys were added in. So cache.keys().next().value on line 13 gives the first key, which is the oldest one.

A cache that keeps at most two answers
Delete the oldest key when the cache is fullPop out in the code editor (opens in a new tab)JavaScript
function memoizeWithLimit(fn, limit = 2) {  const cache = new Map();  return (arg) => {    if (cache.has(arg)) {      const value = cache.get(arg);      cache.delete(arg);      cache.set(arg, value);      return value;    }    const value = fn(arg);    cache.set(arg, value);    if (cache.size > limit) {      const oldest = cache.keys().next().value;      cache.delete(oldest);    }    return value;  };} let calls = 0;const square = memoizeWithLimit((n) => {  calls++;  return n * n;}); console.log(square(2));console.log(square(3));console.log(square(2));console.log(square(4));console.log(square(3));console.log(calls);
Outputthe last line is calls
  1. 4
  2. 9
  3. 4
  4. 16
  5. 9
  6. 4
Try it yourself
Limit: 2 answers

The cache keeps the two keys used most recently. Calling square(4) deletes key 3, so the later square(3) has to run the real function again. That is why calls ends at 4.

Real apps often use a library for this. This small version shows the rule for which key gets deleted.
A saved array can be changed by the callerPop out in the code editor (opens in a new tab)JavaScript
function memoize(fn) {
  let saved;
  return () => saved ??= fn();
}

const getTags = memoize(() => ["tea"]);
const tags = getTags();
tags.push("sugar");
console.log(getTags().join("+"));

This memoize saves just one answer. Line 3 uses ??=, which you'll meet again below: it runs fn() only if saved is still empty.

  1. Line 7 gets the saved array.
  2. Line 8 adds "sugar" to it.
  3. tags and the cache hold the same array, not two copies. So line 9 prints tea+sugar.

If callers must not change cached data, return a copy. Or lock the value with Object.freeze, which stops anyone from changing it.

Memoize async work by saving the promise

PROMISES

An async function returns a promise: an object that stands for a result that will arrive later. Say two parts of a page ask for the same user at the same time. If you save only the finished result, you save it too late. The second request has already started before the first one ends.

So save the promise itself, right away. Then:

  • The second caller gets the same promise, and both wait for the same request.
  • If the promise fails (we say it is rejected), delete its key. Then a retry makes a new request instead of getting the same failed promise back.
Two calls to loadUser("42") at the same time. The 1st call is a miss: it runs the loader and saves the promise. The 2nd call is a hit: it gets the same saved promise. The loader ran once.1st call: missloadUser("42")2nd call: hitloadUser("42")one saved promisepromisethe loader ran once
The first call saves the promise right away, so the second call gets that same promise. That is why first === second is true and calls is 1.
Async memoization shares the promise
A memoize that saves promisesPop out in the code editor (opens in a new tab)JavaScript
function memoizeAsync(fn) {  const cache = new Map();  return (key) => {    if (cache.has(key)) return cache.get(key);    const promise = fn(key).catch((error) => {      cache.delete(key);      throw error;    });    cache.set(key, promise);    return promise;  };} let calls = 0;const loadUser = memoizeAsync(async (id) => {  calls++;  if (id === "bad") throw new Error("not found");  return { id, name: "Ada" };}); (async () => {  const [first, second] = await Promise.all([loadUser("42"), loadUser("42")]);  console.log(first === second);  console.log(calls);  try { await loadUser("bad"); } catch {}  try { await loadUser("bad"); } catch {}  console.log(calls);})();
Outputreal promise run
    Try it yourself
    Save the promise, delete it if rejected

    Two loadUser('42') calls at the same time share one saved promise. Each failed bad load deletes its key, so a retry can try again.

    The output comes from running the same async code shown here. Reset runs it again.
    1. Line 4 returns the saved promise on a hit.
    2. Lines 5 to 8 add a .catch handler. If the promise is rejected, it deletes the key, then throws the error again so the caller still sees it.
    3. Line 9 saves the promise before it finishes.
    4. The output is true (both callers got the same user object), then 1 (the loader ran once), then 3 (each failed retry ran the loader again).
    A real app needs more

    This helper is not a full data cache. A real app also needs:

    • a time limit, after which saved data is fetched again,
    • a separate cache for each signed-in user,
    • a way to cancel a request,
    • clearing the cache after data changes on the server.

    The idea here is the core of all of them: share the promise, and delete failures.

    Lazy values: thunks, getters, and ??=

    STEP THROUGH

    Here are three ways to wait until a value is needed:

    • A thunk is a function you call later to get a value, like () => expensiveValue(). Nothing is worked out until you call it.
    • A getter is a function that runs when you read a property. You write it as get name() { … } inside an object. A lazy getter works out the value on the first read. Then it replaces itself with a normal property that holds the value, so later reads skip the work.
    • ??= is the nullish assignment operator. a ??= b sets a to b only if a is null or undefined. It means "fill this in only if it is empty".
    Real-life analogyA sticky note with a saved answer

    You don't work out every answer before anyone asks. When you need an answer, you write it on a sticky note. The next time, you just read the note.

    In real life: The sticky note is blank at first
    In JavaScript: A thunk or getter holds work for later
    In real life: You write the answer when you need it
    In JavaScript: The first read works out the value
    In real life: Next time, you read the same note
    In JavaScript: The getter replaces itself, or ??= keeps the value
    A getter that saves its value, and ??=
    Step 0 of 14Ready
    Your turn: follow the blue line

    Lazy code waits to do work until a value is asked for. A getter that replaces itself and ??= both make the second read cheap.

    Running in
    1. script
    Next: line 1
      get permissions() {    console.log("compute permissions");    const value = ["read", "write"];    Object.defineProperty(this, "permissions", {      value,      enumerable: true,      configurable: true,    });    return value;  },}; console.log("before");console.log(profile.permissions.join("+"));console.log(profile.permissions.join("+")); const state = {};state.config ??= (() => {  console.log("init config");  return { theme: "dark" };})();state.config ??= { theme: "light" };console.log(state.config.theme);
    CallStoreChangeResultBlue line: runs next. Ran: already executed.
    Recent returnsNothing yet. Start with the blue line.
    A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

    Line 5 uses Object.defineProperty, which adds or replaces a property on an object. Here it swaps the getter for a normal property that holds the array. That is why compute permissions prints only once. On line 23, state.config already holds an object, so the second ??= does nothing and the theme stays dark.

    Lazy lists with generators and iterator helpers

    COUNTERS

    Array methods are eager: each one does its whole job right away. map visits every item. Then filter visits every item. Only then does slice keep the first few.

    A generator is a function written with function*. It gives out values one at a time with yield. After each one, it pauses until someone asks for the next value.

    Iterator helpers are methods such as map, filter, and take that work on a one-at-a-time source like a generator. Node 22 and current Chrome have them.

    • Iterator.from(source) wraps the source so you can use these methods.
    • take(3) stops after 3 values.
    • toArray() collects the values into an array.

    The chain reads like array code. But each number goes through the whole chain before the next number is made.

    Lazy chain versus eager array
    The lazy chainPop out in the code editor (opens in a new tab)JavaScript
    function* naturals() {  let n = 1;  while (true) {    produced++;    yield n++;  }} let produced = 0;let mapped = 0;let tested = 0; const firstEvenSquares = Iterator.from(naturals())  .map((n) => {    mapped++;    return n * n;  })  .filter((square) => {    tested++;    return square % 2 === 0;  })  .take(3)  .toArray(); console.log(firstEvenSquares.join(","));console.log(produced + "/" + mapped + "/" + tested);
    Countersproduced/mapped/tested
    Lazy result4,16,36
    Lazy counters6/6/6
    Eager result4,16,36
    Eager counters1000/1000/1000
    The same work with an eager arrayPop out in the code editor (opens in a new tab)JavaScript
    let produced = 0;let mapped = 0;let tested = 0; const firstEvenSquares = Array.from({ length: 1000 }, (_, index) => {  produced++;  return index + 1;})  .map((n) => {    mapped++;    return n * n;  })  .filter((square) => {    tested++;    return square % 2 === 0;  })  .slice(0, 3); console.log(firstEvenSquares.join(","));console.log(produced + "/" + mapped + "/" + tested);
    Try it yourself

    The lazy chain makes numbers only until it has 3 even squares. The eager array makes, squares, and tests all 1000 numbers before it keeps the first 3.

    Node 22 and current Chrome have iterator helpers such as map, filter, take, and toArray.

    Look at the counter lines. The squares of 1 to 6 are 1, 4, 9, 16, 25, and 36. Three of them are even: 4, 16, and 36. So the lazy chain only has to make the numbers 1 to 6. The eager array makes, squares, and tests all 1000 numbers before it keeps the same three.

    Top: the lazy chain makes only the numbers 1 to 6, keeps 4,16,36, and its counters print 6/6/6. Bottom: the eager array makes all numbers from 1 to 1000, keeps the same 4,16,36, and its counters print 1000/1000/1000.lazy: makes only 1 to 61234564,16,36counters6/6/6eager: makes all 10001, 2, 3, … 10004,16,36counters1000/1000/1000
    Both give 4,16,36. The lazy chain stops once it has three even squares. The eager array works on all 1000 numbers first.

    Where this helps on real sites

    SORT IT

    Use memoization for slow work whose answer depends only on its inputs, such as:

    • building a search index from a list of products,
    • reading a page address and working out which page to show,
    • formatting labels and prices,
    • building a regular expression (a text pattern) once and reusing it,
    • working out what a user is allowed to do,
    • loading a resource that gives the same result every time.

    Use laziness for:

    • panels that most people never open,
    • settings that are rarely read,
    • very long or endless lists,
    • data you handle one piece at a time.
    Choose where to keep the answers
    • formatPrice('USD') for many repeated prices
    • distanceBetween(cityA, cityB)
    • metadataFor(domNode)
    • Date.now()
    • loadUser(id) that returns a promise
    • element.getBoundingClientRect() during scrolling
    Try it yourself
    0 of 6 correct

    Put each case in the safest place to keep its answers. If the answer changes over time, don't memoize it.

    Choose a category for every card. You can change an answer at any time; Reset clears them all.

    A cache holds part of your app's data. Give it a clear name, keep it small, and delete entries as soon as their data is out of date.

    Common misconceptions

    • "Memoization only makes code faster." It also changes when, and how often, the function runs. If the function is not pure, that changes what your program does.
    • "The cache key is a small detail." The key decides what counts as "the same input".
    • "A WeakMap is a smaller Map." It is a different tool, about how long entries live. It takes only object keys, you can't loop over it, and it has no size.
    • "Lazy code is always faster." It is faster only when the work it skips is bigger than the extra code it adds.
    • "Generators are arrays written another way." A generator pauses and goes on. It does not hold all its future values.

    Each row of this table is a common wrong idea. Read across for the better rule and the reason.

    Wrong ideas about caching and laziness, with the better rule
    Wrong ideaBetter ruleWhy
    memoize makes any function faster.Memoize only pure functions that get the same input again and again.If the function is not pure, the cache can give back old data or skip work that had to happen.
    Objects with the same fields are the same key.Map and WeakMap match object keys by which object they are.Two separate object literals are two keys, unless you turn them into the same string on purpose.
    A cache can grow forever.A page that stays open needs a size limit, or only a few possible keys.A Map with no limit keeps all its keys and values in memory.
    Lazy means async.Lazy means "wait until it is needed".A thunk, a getter, a generator, or ??= can be lazy and still run right away when asked.
    A generator works out the whole list first.A generator pauses after each yield.take(3) can stop an endless source after just enough values.

    Practice exercises

    5 EXERCISES
    Exercise 1 · Warm-upPredict what a memoized function prints

    Read the code. Type the three lines it prints, in order.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function memoize(fn) {
      const cache = new Map();
      return (key) => {
        if (cache.has(key)) return cache.get(key);
        const result = fn(key);
        cache.set(key, result);
        return result;
      };
    }
    
    let calls = 0;
    const square = memoize((n) => {
      calls++;
      return n * n;
    });
    console.log(square(3));
    console.log(square(3));
    console.log(calls);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 2 · PracticeWrite the cache-hit check

      Fill in the helper. Then type the name of the Map method that tells you a key is already saved.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function memoize(fn) {
        // create a Map named cache
        return (key) => {
          // return the cached value on a hit
          // otherwise compute, store, and return
        };
      }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 3 · PracticeFind the key bug

        The two objects look the same. Explain why the lookup still misses.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const cache = new Map();
        const first = { id: 1, role: "admin" };
        const second = { role: "admin", id: 1 };
        cache.set(JSON.stringify(first), "saved");
        console.log(cache.get(JSON.stringify(second)) ?? "miss");

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 4 · PracticeUse async memoization on a real site

          Your product page loads /api/user/42. What must the helper do when that promise is rejected?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          function memoizeAsync(fn) {
            const cache = new Map();
            return (key) => {
              if (cache.has(key)) return cache.get(key);
              const promise = fn(key).catch((error) => {
                cache.delete(key);
                throw error;
              });
              cache.set(key, promise);
              return promise;
            };
          }

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 5 · ChallengeCount the work a lazy chain does

            Type the second line the code prints: the counters for the lazy chain.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            function* naturals() {
              let n = 1;
              while (true) {
                produced++;
                yield n++;
              }
            }
            
            let produced = 0;
            let mapped = 0;
            let tested = 0;
            
            const firstEvenSquares = Iterator.from(naturals())
              .map((n) => {
                mapped++;
                return n * n;
              })
              .filter((square) => {
                tested++;
                return square % 2 === 0;
              })
              .take(3)
              .toArray();
            
            console.log(firstEvenSquares.join(","));
            console.log(produced + "/" + mapped + "/" + tested);

            Answer, then press Check. Spacing and letter case don’t matter.

              Quiz

              7 QUESTIONS
              Memoization and laziness quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which function is safe to memoize?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does the memoized Fibonacci code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function memoize(fn) {
                  const cache = new Map();
                  return function memoized(arg) {
                    if (cache.has(arg)) return cache.get(arg);
                    const result = fn(arg);
                    cache.set(arg, result);
                    return result;
                  };
                }
                
                let calls = 0;
                let fib;
                fib = memoize((n) => {
                  calls++;
                  return n < 2 ? n : fib(n - 1) + fib(n - 2);
                });
                
                console.log(fib(6));
                console.log(calls);
                console.log(fib(6));
                console.log(calls);

                Choose an answer to see the explanation.

              3. Question 3 of 7What does the JSON key example print last?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const cache = new Map();
                const first = { id: 1, role: "admin" };
                const second = { role: "admin", id: 1 };
                cache.set(JSON.stringify(first), "saved");
                console.log(cache.get(JSON.stringify(second)) ?? "miss");

                Choose an answer to see the explanation.

              4. Question 4 of 7Why use a WeakMap for a cache whose keys are objects?

                Choose an answer to see the explanation.

              5. Question 5 of 7What does the async memoization code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function memoizeAsync(fn) {
                  const cache = new Map();
                  return (key) => {
                    if (cache.has(key)) return cache.get(key);
                    const promise = fn(key).catch((error) => {
                      cache.delete(key);
                      throw error;
                    });
                    cache.set(key, promise);
                    return promise;
                  };
                }
                
                let calls = 0;
                const loadUser = memoizeAsync(async (id) => {
                  calls++;
                  if (id === "bad") throw new Error("not found");
                  return { id, name: "Ada" };
                });
                
                (async () => {
                  const [first, second] = await Promise.all([loadUser("42"), loadUser("42")]);
                  console.log(first === second);
                  console.log(calls);
                  try { await loadUser("bad"); } catch {}
                  try { await loadUser("bad"); } catch {}
                  console.log(calls);
                })();

                Choose an answer to see the explanation.

              6. Question 6 of 7What does the lazy getter example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const profile = {
                  get permissions() {
                    console.log("compute permissions");
                    const value = ["read", "write"];
                    Object.defineProperty(this, "permissions", {
                      value,
                      enumerable: true,
                      configurable: true,
                    });
                    return value;
                  },
                };
                
                console.log("before");
                console.log(profile.permissions.join("+"));
                console.log(profile.permissions.join("+"));
                
                const state = {};
                state.config ??= (() => {
                  console.log("init config");
                  return { theme: "dark" };
                })();
                state.config ??= { theme: "light" };
                console.log(state.config.theme);

                Choose an answer to see the explanation.

              7. Question 7 of 7What does the lazy chain print for the first three even squares?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function* naturals() {
                  let n = 1;
                  while (true) {
                    produced++;
                    yield n++;
                  }
                }
                
                let produced = 0;
                let mapped = 0;
                let tested = 0;
                
                const firstEvenSquares = Iterator.from(naturals())
                  .map((n) => {
                    mapped++;
                    return n * n;
                  })
                  .filter((square) => {
                    tested++;
                    return square % 2 === 0;
                  })
                  .take(3)
                  .toArray();
                
                console.log(firstEvenSquares.join(","));
                console.log(produced + "/" + mapped + "/" + tested);

                Choose an answer to see the explanation.

              Key takeaways

              • Memoize a function only if the same inputs always give the same answer. Pick a key that says what "the same inputs" means.
              • Show what happens with call counters and printed output, not with timings.
              • A Map, nested Maps, a WeakMap, and a Map with a size limit each keep answers for a different length of time. Pick the one that fits.
              • For async work, save the promise right away, and delete it if it is rejected.
              • Lazy values and lazy lists do work only when asked, so they can skip most of the work.

              Next comes advanced recursion. The same habits help there: count which calls really run, and keep saved data where you can see it.

              CompleteFrontend Clear concepts. Working examples.