completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Weak references & ephemerons

Learn how collectors treat WeakMap, WeakRef, finalizers, ephemerons, and job-level liveness without relying on unreliable cleanup timing.

By the end, you can
  • 01
    Tell strong and weak references apartExplain why a weak reference can read an object without keeping it alive, and find the strong reference that does keep it.
  • 02
    Explain ephemeronsUse a small marking model to show why a WeakMap value is kept only after its key is kept.
  • 03
    Avoid timing bugsUse WeakRef and finalizers safely: know the current-job promise, and write cleanup that must happen yourself.

Weak references in the collector

The garbage collector (the collector, for short) is the part of the JavaScript engine that frees memory. It keeps every object your program can still reach and throws the rest away.

An object is reachable when your code can get to it from a starting point, such as a variable that is still in use. These starting points are called roots.

Most references are simple. If a reachable object points at another object, the collector keeps both. A few built-in tools work differently. They let your code look at an object, or attach extra information to it, without keeping that object alive.

Definition

A weak reference points at an object but does not keep it alive. An ephemeron is a pair of a key and a value. The collector keeps the value only if something else already keeps the key alive.

This lesson looks at what the collector does with these tools. Two earlier lessons show how to use them in everyday code: WeakMap and WeakSet and WeakRef and finalization.

The previous lesson showed how the collector splits its work into small pieces, so the page does not freeze. Weak references add one more rule to that work. The next lesson, Garbage collection across engines & the DOM, compares how real engines handle JavaScript objects together with the objects of a web page.

Strong and weak references

A strong reference is an ordinary reference, like a property or a variable. The collector follows it. If a reachable object has a property that points at a cart, the cart is reachable too.

People who study collectors often draw objects as dots and references as lines between them. Such a line is called an edge. So a strong reference is also called a strong edge, and a weak reference a weak edge.

A WeakRef is a built-in object that holds a weak reference to one object, its target. Its deref() method gives you the target back. It gives undefined if the target was already thrown away.

Read the cart through a strong and a weak referencePop out in the code editor (opens in a new tab)JavaScript
const cart = { price: 3 };const strong = { cart };const weak = new WeakRef(cart); console.log(strong.cart.price);console.log(weak.deref()?.price);

In plain words, line by line:

  1. Line 1 makes a cart with a price of 3.
  2. Line 2 makes an ordinary object, strong, that holds the cart. This is a strong reference.
  3. Line 3 makes a WeakRef called weak that points at the same cart. This is a weak reference.
  4. Line 5 reads the price through strong and prints 3.
  5. Line 6 reads the price through weak and prints 3 too. The ?. means "if deref() gave undefined, give undefined instead of an error."

Both reads work because strong still holds the cart.

A tag called strong has an arrow to the cart handle, labeled keeps it alive. A tag called weak has an arrow to the cart, labeled only looks. The cart holds a price card with 3 on it.strongkeeps it aliveweakonly looksprice: 3cart
strong holds the cart, so the cart stays alive. weak only looks at it, so both reads print 3 only because strong still holds it.
Real-life analogyBalloons on strings

If you hold a balloon's string, the balloon stays with you. If you only look at a balloon, you can see it while someone else holds the string. Looking never stops it from floating away when nobody holds the string.

In real life: Holding a balloon string
In JavaScript: A strong reference keeps the object alive
In real life: Only looking at a balloon
In JavaScript: A weak reference can read the object
In real life: Letting go of the string
In JavaScript: The last strong reference goes away
In real life: Balloon floating away
In JavaScript: The collector may later throw the object away

Weak does not mean broken or hidden. It means: "do not keep the object alive because of this reference." So a cache (a store of saved results) can attach extra information to an object without changing how long that object lives.

A WeakMap entry depends on its key

A WeakMap is a map whose keys must be objects. You store a value with set(key, value) and read it with get(key). It is useful for extra information about an object that you don't want to put inside the object.

Start with the simple case: the key is still in use elsewhere.

Read extra information for a key that is in usePop out in the code editor (opens in a new tab)JavaScript
const user = { name: "Asha" };const notes = new WeakMap();notes.set(user, "likes tea");console.log(notes.get(user));

In plain words, line by line:

  1. Line 1 makes a user object.
  2. Line 2 makes the map.
  3. Line 3 stores the string "likes tea" with user as the key.
  4. Line 4 reads it back and prints likes tea.

The variable user is an ordinary strong reference to the key. So nothing surprising happens yet.

Real-life analogyA name tag at a party

Think of a name tag on a guest at a party. The tag is useful while the guest is at the party. But the tag never keeps the guest there.

In real life: A guest at the party
In JavaScript: A key that is reachable from the roots
In real life: A name tag for that guest
In JavaScript: A WeakMap entry
In real life: A note written on the tag
In JavaScript: The value stored with the key
In real life: The guest leaves the party
In JavaScript: The key is no longer reachable

The important part is the value. An ordinary Map keeps both its keys and its values alive. A WeakMap does not. It keeps a value alive only after the collector has found that the key is alive for some other reason.

Ephemerons: the key first, then the value

The collector's name for one WeakMap entry is an ephemeron. It is a rule, not an ordinary reference: "keep the value only if the key is already kept."

To find live objects, the collector marks them. Marking means putting a "keep" sign on an object. The collector starts at the roots and follows strong references. Every object it reaches gets marked. At the end, unmarked objects are thrown away.

With ephemerons, the collector works in this order:

  • First, it marks everything it can reach through strong references.
  • Then it looks at each entry. If the key is marked, it marks the value too.
  • A newly marked value may point at more objects, so it goes back to the first step. It stops when a full round marks nothing new.
The key decides about the valuePop out in the code editor (opens in a new tab)JavaScript
const roots = ["user"];const entry = { key: "user", value: "notes" };console.log(roots.includes(entry.key) ? entry.value : "nothing");

This small program is a model, not the real collector. Line 1 says user is a root. Line 2 describes one entry: the key is user and the value is notes. Line 3 uses includes to check whether the key is in the roots list. The ? : picks entry.value if it is, and "nothing" if it is not. The key is a root, so line 3 prints notes.

The real collector repeats this check for many objects. The next model does that with a loop.

A model that marks until nothing changesPop out in the code editor (opens in a new tab)JavaScript
const roots = ["user"];const graph = { user: [] };const entries = [{ key: "user", value: "notes" }]; function markEphemerons(roots, graph, entries) {  const marked = new Set(roots);  let changed = true;  while (changed) {    changed = false;    for (const name of [...marked]) {      for (const child of graph[name] ?? []) {        if (!marked.has(child)) { marked.add(child); changed = true; }      }    }    for (const entry of entries) {      if (marked.has(entry.key) && !marked.has(entry.value)) {        marked.add(entry.value); changed = true;      }    }  }  return [...marked].sort();} console.log(markEphemerons(roots, graph, entries).join(","));

Read it in parts:

  1. Lines 1 to 3 set up the data: one root, user, and one entry from user to notes. graph lists the strong references of each object; user has none.
  2. Line 6 marks the roots. A Set is a list with no repeats.
  3. Line 8 repeats the work as long as the last round marked something new.
  4. Lines 10 to 14 follow strong references and mark every object they reach.
  5. Lines 15 to 19 apply the entry rule: if the key is marked, mark the value.
  6. Line 24 prints the marked names in A-to-Z order: notes,user.

When a full round marks nothing new, the result stops changing. That stable result is called a fixpoint (a fixed point).

Left: roots is user. The user card is marked first, an arrow goes to the notes card, which is then marked. Result: notes,user. Right: roots is empty. User has no root, notes is not marked. Result: nothing.roots = ["user"]usernotesmarked firstthen markedmarked: notes,userroots = []usernotesno rootnot markedmarked: nothing
The entry links the key user to the value notes. On the left, a root reaches user, so notes is marked too. On the right, nothing reaches user, so nothing is marked.
Step through the key-first marking rule
Step 0 of 5Ready
Your turn: follow the blue line

Step through a small ephemeron marking loop. The value is kept because its key was already reachable another way.

Running in
  1. script
Next: line 1
const graph = { user: [] };const entries = [{ key: "user", value: "notes" }]; const result = markEphemeronsForReplay(roots, graph, entries);console.log(result.marked.join(","));
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.

The step-through calls the same model function as the playground below. Real engines make this faster with queues and other tricks. The rule stays the same: the key first, then the value.

Playground: is the key a root?
The marking modelJavaScript
const roots = ["user"];const graph = { user: [] };const entries = [{ key: "user", value: "notes" }]; function markEphemerons(roots, graph, entries) {  const marked = new Set(roots);  let changed = true;  while (changed) {    changed = false;    for (const name of [...marked]) {      for (const child of graph[name] ?? []) {        if (!marked.has(child)) { marked.add(child); changed = true; }      }    }    for (const entry of entries) {      if (marked.has(entry.key) && !marked.has(entry.value)) {        marked.add(entry.value); changed = true;      }    }  }  return [...marked].sort();} console.log(markEphemerons(roots, graph, entries).join(","));
What the model marked2 rounds
markednotes,user

The loop repeats until a full round marks nothing new.

Try it yourself

user is a root, so it is marked first. Then the entry lets the model mark notes too.

This runs the lesson's small marking model. It shows the rule; it does not look inside a real engine's memory.

Why a value cannot save its own key

The order matters most when a value points back at its own key. Suppose the value notes has a reference to its key, user.

If the collector looked at the value first, notes would mark user. Then user would mark notes. The two would keep each other alive forever, even though your program can't reach either one.

A value that points back does not start markingPop out in the code editor (opens in a new tab)JavaScript
const roots = [];const graph = { user: [], notes: ["user"] };const entries = [{ key: "user", value: "notes" }];console.log(markEphemerons(roots, graph, entries).join(",") || "nothing");

In plain words, line by line:

  1. Line 1 has no roots at all.
  2. Line 2 says notes has a strong reference back to user.
  3. Line 3 describes the same entry as before.
  4. Line 4 runs the model. The || "nothing" part prints nothing when no name was marked. Nothing marked user, so the entry never marks notes. The reference back to user is never followed.
roots is empty. The notes card has an arrow back to the user card, labeled points back. Under user: key, not marked. Under notes: value, not marked. Result: prints nothing.roots = []userpoints backnoteskey: not markedvalue: not markedprints nothing
notes points back to user, but no root reaches user. The collector never looks inside notes, so line 4 prints nothing.

So "weak key, strong value" is not the full story for WeakMap. The value is kept only after the collector knows the key is alive.

The order keeps it safe

First mark everything reachable the normal way. Then let each marked key mark its value. Repeat until nothing new is marked. This order stops a WeakMap entry from keeping itself alive.

When a WeakRef target can disappear

A WeakRef lets you read its target only while the target still exists. Right now, deref() can return the object. After a later collection, it can return undefined. A WeakRef never promises that the object will stay.

Ask a WeakRef twice, right awayPop out in the code editor (opens in a new tab)JavaScript
const user = { name: "Asha" };const ref = new WeakRef(user);console.log(ref.deref()?.name);console.log(ref.deref() === undefined ? "gone" : "here");

In plain words, line by line:

  1. Line 1 makes an object for Asha.
  2. Line 2 makes a WeakRef to it.
  3. Line 3 prints Asha.
  4. Line 4 prints here, because the object still exists right now.

Later, after the last strong reference is gone, the same check on line 4 may print gone.

A FinalizationRegistry lets you register a callback (a function the engine calls for you later). The engine may call it some time after a registered object is thrown away. This callback is called a finalizer. It comes with no promises about time:

  • It may run much later.
  • It may run at a moment your program did not expect.
  • It may never run, for example if the page closes first.

So a finalizer is only for optional record keeping. Never use it to close a file, save a draft, release a lock, or cancel a request.

The table compares four similar ideas. Read each row as: "this thing follows this rule, so think of it like this."

Four weak ideas and what each one promises
ThingWhat the collector doesHow to think about it
Strong referenceKeeps its target alive while the object holding it is alive.An ordinary property, such as strong.cart. Like holding a balloon's string.
WeakRefCan give you its target, but does not keep it alive.A look at an object that may be gone later. Always check deref() for undefined.
WeakMap entryKeeps its value only after its key is kept alive some other way.Extra information attached to a user object that is still in use.
FinalizerMay run some time after its object is thrown away, or never.Optional record keeping. Never use it to close or save something.

For cleanup that must happen, write code that does it yourself:

  • Call the object's own close() method.
  • Use a using declaration where it is supported. It calls the object's cleanup method for you when the block ends.
  • Use an AbortController to cancel work, such as a fetch request.

These actions happen because your program asked for them, at a time you choose.

A target stays alive until the current job ends

A weak reference would be hard to use if its target could vanish between two reads on nearby lines. So JavaScript gives one small promise. When you create a WeakRef, or get an object from deref(), that object stays alive until the current job ends.

Two words matter here:

  • A job is the code that is running right now, from start to end without a pause, plus any promise callbacks it queues before the next task.
  • A task is a separate piece of work that the engine runs later, such as a timer callback or a click handler.
Read the target twice in one jobPop out in the code editor (opens in a new tab)JavaScript
let user = { name: "Asha" };const ref = new WeakRef(user);user = null; const first = ref.deref();const second = ref.deref();console.log(first === second, second?.name);

In plain words, line by line:

  1. Line 1 makes Asha's object.
  2. Line 2 makes a WeakRef to it.
  3. Line 3 sets user to null, so the only strong reference is gone.
  4. Lines 5 and 6 read the target twice.
  5. Line 7 prints true Asha: both reads got the same object.
Step through two reads in one job
Step 0 of 5Ready
Your turn: follow the blue line

Step through the current job promise: an object you just got from a WeakRef stays alive until the code running now, and its promise callbacks, are done.

Running in
  1. script
Next: line 1
const ref = new WeakRef(user);user = null; const first = ref.deref();const second = ref.deref();console.log(first === second, second?.name);
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.
Window called this job: first deref gives Asha, second deref gives Asha. An arrow labeled later goes to a window called later task: deref gives Asha or undefined.this job1st deref: Asha2nd deref: Ashasame objectlaterlater taskAsha or undefinedcheck first
Inside one job, both deref() calls get Asha, so line 7 prints true Asha. In a later task, the object may be gone, so check for undefined.

A later timer, a browser event, or any other later task is not covered by this promise. Don't save a weak target and expect it to exist when a later callback runs. Call deref() again there, and check for undefined.

Checking it in Node

Browser code cannot make the collector run. It should also never depend on when the collector runs.

For testing, Node can turn on a test-only function, gc(), when you start Node with the --expose-gc flag. Calling gc() asks for a collection right away. The three checks below use it. They print short words, never memory sizes.

Node only: an object kept only by a WeakRefJavaScript
const laterTask = () => new Promise((resolve) => setTimeout(resolve, 0)); function makeWeakOnly() {  let value = { label: "weak-only", data: new Array(2000).fill("tea") };  const ref = new WeakRef(value);  value = null;  return ref;} const ref = makeWeakOnly();let collected = false;for (let attempt = 0; attempt < 12; attempt += 1) {  await laterTask();  globalThis.gc();  await laterTask();  if (ref.deref() === undefined) { collected = true; break; }}console.log("weak collected", collected);

In plain words:

  1. Line 1 defines laterTask. It waits until a later task by using setTimeout.
  2. Lines 3 to 8 make an object (line 4), wrap it in a WeakRef (line 5), and drop the strong variable (line 6). Only the WeakRef is returned.
  3. Lines 12 to 17 try up to 12 times: wait for a later task, call gc(), wait again, and check whether deref() gives undefined.
  4. Line 18 prints weak collected true in the tested Node version.

The loop tries several times because nobody can promise which attempt will work, or how long it takes.

Node only: a WeakMap value whose key is goneJavaScript
const laterTask = () => new Promise((resolve) => setTimeout(resolve, 0)); function makeEntry() {  let key = { name: "Asha" };  let value = { label: "notes", data: new Array(2000).fill("tea") };  const valueRef = new WeakRef(value);  const map = new WeakMap();  map.set(key, value);  key = null;  value = null;  return valueRef;} const valueRef = makeEntry();let collected = false;for (let attempt = 0; attempt < 12; attempt += 1) {  await laterTask();  globalThis.gc();  await laterTask();  if (valueRef.deref() === undefined) { collected = true; break; }}console.log("ephemeron value collected", collected);

In plain words:

  1. Line 6 gives the value its own WeakRef. Now the check can watch the value without keeping it alive.
  2. Line 8 stores the key and the value in the WeakMap.
  3. Lines 9 and 10 drop both variables, so nothing else reaches the key.
  4. Lines 16 to 21 try up to 12 times, the same way as before.
  5. Line 22 prints ephemeron value collected true. Once the key was gone, the value was thrown away too.
Node only: two steps in the same jobJavaScript
let user = { name: "Asha" };const ref = new WeakRef(user);user = null;globalThis.gc();console.log("same job", ref.deref()?.name);

Line 3 drops the strong variable, and line 4 even asks for a collection. Line 5 still prints same job Asha, because the job has not ended yet.

This only checks the job promise. Never call a test-only function like gc() in app code. There is no check for finalizers here: their timing is not reliable enough to test.

Using weak tools carefully

A good use for WeakMap is extra information about an object that another part of the app owns. To own an object means to decide how long it lives. For example, a layout helper might store a row number for each user object without adding a property to that object.

Store notes beside an object you don't ownPop out in the code editor (opens in a new tab)JavaScript
const user = { name: "Asha" };const layoutNotes = new WeakMap();layoutNotes.set(user, { row: 3 });console.log(layoutNotes.get(user)?.row);

In plain words, line by line:

  1. Line 1 is the user object. Another part of the app owns it.
  2. Line 2 makes a private map for layout notes.
  3. Line 3 stores row 3 for this user.
  4. Line 4 prints 3.

When nothing else in the app reaches the user, the map does not keep its notes alive.

Don't pick a weak tool just to avoid deciding who owns an object. Weak tools have limits:

  • You can't loop over the entries of a WeakMap.
  • An entry can disappear any time after its key is gone.
  • A weak map is not a cache with a size limit.

Use an ordinary Map when you need to loop over entries, count them, remove old ones by your own rule, or save them.

Which rule does each card describe?
  • cart.user points to a user object.
  • A WeakRef can later return undefined.
  • A marked key lets its WeakMap value be marked.
  • A key that nothing reaches does not keep its value.
  • Two deref() calls on nearby lines get the same target.
  • An event listener that is still added uses a cart.
Try it yourself
0 of 6 correct

Sort each card by the rule it describes. Every card uses this lesson's cart, user, and notes examples.

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

Common mistakes with weak references

  • "WeakMap values are always kept." A value is kept only while its key is reached some other way.
  • "A WeakRef keeps the object for me." It only looks. deref() may give undefined later.
  • "A finalizer can close things." It may run late or never, so cleanup that must happen needs your own code.
  • "Apps can call gc()." That function exists only in Node with a test flag. Browsers don't offer it.
  • "Two reads on nearby lines can give different answers." Inside one job, the target stays alive, so both reads match.

Read each row as: "this term means this, and it is not that."

Terms that are easy to mix up
TermWhat it really meansDon't mix it up with
WeakMap valueIt is kept only while its key is marked some other way.An ordinary property that the map always keeps.
WeakRef.deref()It gives the object now, but may give undefined in a later task.A handle that keeps the object for you.
Finalizer callbackA notice that may come late or never.A cleanup step that is sure to run.
Current jobThe code running now, plus the promise callbacks it queues before the next task.Every later timer or click callback.

A simple habit helps:

  • Use ordinary strong references by default.
  • Use a weak tool when the object's owner should decide how long it lives.
  • Never make your program depend on when the collector runs.

Practice exercises

Exercise 1 · Warm-upPredict the value for a key in use

Read the four lines. Type what line 4 prints while user is still reachable.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const user = { name: "Asha" };
const notes = new WeakMap();
notes.set(user, "likes tea");
console.log(notes.get(user));

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

    Exercise 2 · Warm-upPredict what gets marked

    Predict the marked names. Type them in the printed order, separated by a comma.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const marked = new Set(["user"]);
    const entry = { key: "user", value: "notes" };
    if (marked.has(entry.key)) marked.add(entry.value);
    console.log([...marked].sort().join(","));

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

      Exercise 3 · PracticeA value that points back

      The value points back to its key, but no root reaches the key. What does the program print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const roots = [];
      const entry = { key: "user", value: "notes" };
      const marked = new Set(roots);
      if (marked.has(entry.key)) marked.add(entry.value);
      console.log([...marked].join(",") || "nothing");

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

        Exercise 4 · PracticeRead a WeakRef later

        In a later task, after a collection, what can deref() return instead of the object?

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

          Exercise 5 · ChallengeChoose cleanup that must happen

          Your app must cancel an upload when a panel closes. What kind of cleanup should it use?

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

            Exercise 6 · ChallengeUse a WeakMap on a real website

            A profile list owns user objects. A layout helper wants to store a row number for each user. What should the helper use as the key in its WeakMap?

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

              Check your understanding

              For each question, first ask which idea it is about: a strong reference, a key that must be marked first, a later read, or the current job.

              Weak references and ephemerons quiz · 6 questionsScore: first tries count
              1. Question 1 of 6What does the first WeakMap example print while user is reachable?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const user = { name: "Asha" };
                const notes = new WeakMap();
                notes.set(user, "likes tea");
                console.log(notes.get(user));

                Choose an answer to see the explanation.

              2. Question 2 of 6What makes an ephemeron different from an ordinary reference?

                Choose an answer to see the explanation.

              3. Question 3 of 6A WeakMap value points back to its own key, but nothing else reaches the key. What happens in the model?

                Choose an answer to see the explanation.

              4. Question 4 of 6What does this code print right away?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const ref = new WeakRef({ name: "Asha" });
                console.log(ref.deref() === undefined ? "gone" : "here");

                Choose an answer to see the explanation.

              5. Question 5 of 6How should you handle cleanup that must happen?

                Choose an answer to see the explanation.

              6. Question 6 of 6For this lesson's rule, what does the current job include?

                Choose an answer to see the explanation.

              Key takeaways

              • A strong reference keeps its target alive. A weak reference can read its target without keeping it alive.
              • A WeakMap entry is an ephemeron: the collector marks the value only after the key is marked, and repeats until nothing changes.
              • A value that points back to its key cannot keep the entry alive by itself.
              • deref() can return undefined later. A finalizer may run late or never, so never use it for cleanup that must happen.
              • Creating a WeakRef or reading it with deref() keeps the target alive until the current job ends.
              • Use weak tools for optional extra information, not to avoid deciding who owns an object.

              Remember the one-liner.
              Weak tools can look at an object, or attach information to it, without keeping that object alive.

              Coming next: Garbage collection across engines & the DOM.

              CompleteFrontend Clear concepts. Working examples.