cf.completefrontendCode editorOpen lab
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
    Trace weak edgesExplain why a weak observation can report an object without keeping that object alive, and identify the strong owner that still does.
  • 02
    Explain ephemeronsUse a fixed-point marking model to explain why a WeakMap value depends on its key.
  • 03
    Avoid timing bugsUse WeakRef and finalizers safely, including the narrow current-job liveness guarantee and explicit cleanup.

Weak references in the collector

Most JavaScript references are simple: when a reachable object points at another object, the collector keeps both. Weak APIs make a deliberate exception. They let code observe or attach metadata without turning that observation into ownership.

Definition

A weak reference is a reference that does not keep its target reachable. An ephemeron is a conditional association: its value is kept only when its key is already reachable some other way.

This is an engine lesson, not an API catalogue. The published WeakMap and WeakSet and WeakRef and finalization lessons teach everyday usage. Here we ask what the collector must do so those APIs do not accidentally keep objects alive.

The previous lesson explained how collectors schedule marking work without freezing a page. Weak edges add one more rule to that marking work. The next lesson, Garbage collection across engines & the DOM, compares how real engines join JavaScript objects with browser and host objects.

Weak versus strong edges

A strong edge is an ordinary reference that the marker follows. If a reachable cart has a user property, that property makes the user reachable too. The edge is part of the normal object graph.

Strong and weak reads while the object is livePop 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);

Line 1 creates a cart. Line 2 creates an ordinary object that strongly points at it. Line 3 creates a WeakRef that can observe the same cart. Lines 5 and 6 both print 3 while the strong edge still keeps the cart alive.

Real-life analogyBalloons on strings

Holding a balloon's string keeps it with you. Only looking at the balloon lets you see it while someone else holds it, but looking never stops it from floating away when nobody holds the string.

In real life: Holding a balloon string
In JavaScript: A strong edge keeps a target live
In real life: Only looking at a balloon
In JavaScript: A weak edge can observe a target
In real life: Letting go of the string
In JavaScript: Dropping the last strong path
In real life: Balloon floating away
In JavaScript: The target may later be collected

Where the analogy stops: A collector does not watch balloons or choose a visible moment. It follows reachability and may collect later.

Weak does not mean broken or hidden. It means “do not use this edge as a reason to preserve the target.” That distinction lets a cache attach optional information without silently changing the lifetime of every object it sees.

A WeakMap entry is conditional

Start with the small, ordinary part of WeakMap. Its key is an object, and while another strong path reaches that key, code can ask for the associated value. The map is useful for metadata that belongs beside an object rather than inside it.

Read metadata for a live keyPop 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));

Line 1 creates user. Line 2 creates the map. Line 3 attaches the string "likes tea" to that user, and line 4 prints likes tea. The variable user is an ordinary strong path, so this example says nothing surprising yet.

Real-life analogyA name tag at a party

Think of a name tag on a party guest. The tag stays useful while the guest is at the party, but the tag never keeps the guest there, even if the tag mentions the guest again.

In real life: A guest at the party
In JavaScript: A key reachable from roots
In real life: A name tag for that guest
In JavaScript: A WeakMap entry
In real life: A note printed on the tag
In JavaScript: The associated value
In real life: Guest leaves the party
In JavaScript: Key becomes unreachable

Where the analogy stops: A real tag cannot run a marking loop. The analogy only explains that a tag does not keep a guest at the party.

The crucial part is the value. A normal map would strongly preserve both key and value through the map. A WeakMap must instead wait for a separate proof that the key is live before it treats the value as live.

Ephemerons and a marking fixpoint

An ephemeron is the collector's name for the conditional rule behind a WeakMap entry. It is not simply a weak pointer from the map to a key. The collector must first mark ordinary strong paths, then revisit entries whose keys have become marked.

A tiny ephemeron resultPop 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");

Line 1 makes user live in this tiny model. Line 2 describes an entry, and line 3 prints notes because the key appears in roots. A real collector repeats this idea through a graph, rather than checking one entry with includes.

A fixed-point teaching modelPop 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(","));

Line 5 starts the model with the roots. Lines 8 through 14 follow ordinary strong edges. Lines 16 through 20 apply the special rule: only a marked key unlocks its value. The loop repeats until a full pass changes nothing, which is called a fixpoint.

Step through an ephemeron marking pass
Step 0 of 5Ready
Your turn: follow the blue line

Replay a small ephemeron marking loop. The value is kept because the key was already reachable another way.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const graph = { user: [] };const entries = [{ key: "user", value: "notes" }]; const result = markEphemeronsForReplay(roots, graph, entries);console.log(result.marked.join(","));
CallStoreChangeResultRun = next line. 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 replay calls the same model function used by the playground. It is intentionally small: real engines optimize this work with queues and internal metadata, but the conditional “key first, then value” rule is the important part to keep.

Playground: mark a WeakMap entry
Ephemeron 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(","));
Model result2 passes
markednotes,user

The loop repeats until a full pass adds no names.

Try it yourself

user is marked first, so the model marks notes on a later pass.

This runs the lesson's small fixed-point model. It explains the rule; it does not inspect an engine heap.

Why a value cannot rescue its key

The rule matters most in a shape that feels circular. Imagine a WeakMap value that points back to its own key. If the collector followed the value first, the value could mark the key, and a dead entry would incorrectly keep itself alive forever.

A back-reference 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");

Line 1 gives the model no roots. Line 2 says notes points back to user. Line 3 still describes the conditional entry. Line 4 prints nothing, because the key was never marked from elsewhere, so the entry never unlocks notes.

This is why “weak key, strong value” is an incomplete mental model for WeakMap. The value is not simply strong in all circumstances. It is conditionally strong after the collector has proof that the key belongs to the live graph.

The order is the safety feature

First find normal reachability. Then let marked keys reveal their values. Repeat until no new objects appear. That ordering prevents a WeakMap entry from creating its own reason to survive.

WeakRef and finalizer timing

A WeakRef gives an optional read of a target. Calling deref() can return the object, or it can return undefined after a later collection. It is not a lease, lock, or promise that the object will remain around.

A weak read can be optionalPop 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");

Line 1 creates an object and line 2 makes a weak observation. Line 3 prints Asha in this immediate job. Line 4 prints here for the same reason. In a later job, after the last strong path disappears, the same expression may print gone.

FinalizationRegistry can schedule a callback after an associated target is collected. The callback may run much later, run in a different environment moment, or not run before shutdown. It is a notification for optional bookkeeping, never the place to close a required file, save a draft, release a lock, or cancel a request.

Similar weak concepts, different promises
ThingCollector ruleGood mental model
Strong edgeKeeps its target reachable while the source is reachable.A normal object property such as cart.user.
Weak observationMay report a target, but does not keep it alive.A WeakRef created for an advanced cache.
WeakMap entryKeeps its value only after its key is otherwise reachable.Metadata attached to a live user object.
FinalizerA later, optional notification after collection.Never a required close or save operation.

For required cleanup, write an explicit operation. A resource owner can call its own close(), use the language's using patterns where available, or connect work to an AbortController. Those actions happen because your program requested them.

Keeping a target alive across a job

Weak APIs would be hard to use if a target could vanish between two nearby reads in one piece of code. JavaScript therefore gives a narrow guarantee: creating a WeakRef or getting an object from deref() keeps that target alive through the current job.

Two reads in one synchronous 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);

Line 1 creates Asha's object. Line 2 creates its WeakRef, and line 3 drops the local strong reference. Lines 5 and 6 read the target twice. Line 7 prints true Asha: both reads see the same object during this synchronous run.

Step through two reads in one job
Step 0 of 5Ready
Your turn: follow the blue line

Replay the JavaScript current job guarantee: a target observed through a new WeakRef stays available until this synchronous job and its promise jobs finish.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const ref = new WeakRef(user);user = null; const first = ref.deref();const second = ref.deref();console.log(first === second, second?.name);
CallStoreChangeResultRun = next line. 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.

A job here means the synchronous code currently running plus promise jobs it queues before the next task. A later timer, browser event, or other task is outside that guarantee. Do not save a weak target and assume it will still exist when a later callback runs.

Cheap Node runtime evidence

Production browser code cannot force collection, and it should not depend on a collection schedule. For a stable lesson check, Node can expose a test hook with --expose-gc. These Node-only probes wait for later tasks, retry collection, and assert simple words instead of heap sizes.

Node-only weak-only target probeJavaScript
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);

Line 4 creates an object that is returned only through a WeakRef. Lines 11 through 15 cross later tasks and retry gc(). Line 18 eventually prints weak collected true in the tested Node process. The loop avoids claiming a particular millisecond or attempt.

Node-only WeakMap value probeJavaScript
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);

The value has a separate WeakRef so the probe can observe it without owning it. Lines 7 and 8 put the key and value into the WeakMap, then drop both locals. With no other key path, a later collection can make the value WeakRef return undefined, and the probe prints ephemeron value collected true.

Node-only same-job probeJavaScript
let user = { name: "Asha" };const ref = new WeakRef(user);user = null;globalThis.gc();console.log("same job", ref.deref()?.name);

Even after line 4 requests collection, line 5 prints same job Asha. This is evidence for the job rule, not an invitation to call a test-only hook from application code. The lesson intentionally omits finalizer callback evidence because it is not stable enough to teach as a promise.

Use weak APIs carefully

A good reason for WeakMap is metadata whose lifetime should match an object that somebody else owns. For example, a UI library might associate measured layout details with a user object without adding a public property to that user object.

Keep cache metadata beside its ownerPop 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);

Line 1 is the object another part of the app owns. Line 2 creates private metadata storage. Line 3 records a row number, and line 4 prints 3. When no ordinary application path reaches the user, the map should not keep its notes alive on its own.

Do not choose a weak collection just to avoid deciding ownership. Weak collections cannot be enumerated, their entries can disappear, and they do not replace a bounded cache. Use an ordinary Map when your program needs reliable iteration, counts, eviction policy, or persistence.

Which weak-reference rule applies?
  • cart.user points to a user object.
  • A WeakRef can later return undefined.
  • A marked key unlocks its WeakMap value.
  • A dead key does not keep its metadata value.
  • Two synchronous deref() calls see the same target.
  • A live event listener captures a cart.
Try it yourself
0 of 6 correct

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

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

Common weak-reference mistakes

  • “WeakMap values are always strong.” They are conditional on their keys being otherwise reachable.
  • “WeakRef gives a stable object handle.” It gives an optional observation that may be undefined later.
  • “A finalizer closes resources.” Its timing is optional, so required cleanup must be explicit.
  • “Calling `gc()` is an application feature.” The Node hook is test-only; browsers do not provide that promise.
  • “Two weak reads can randomly disagree in one line of code.” The current job protects the observed target.
Terms that are easy to blur together
TermWhat it really meansDo not confuse it with
WeakMap valueConditional on a live key during marking.An ordinary strong property of the map.
WeakRef.deref()A snapshot that may be an object or undefined later.A stable ownership handle.
Finalization callbackBest-effort notification at an unspecified later time.A required cleanup hook.
Current jobSynchronous code plus promise jobs before the next task.Every later timer or event callback.

The useful discipline is simple: use ordinary strong ownership by default, use a weak association when the object's owner should control its lifetime, and never make correctness depend on the collector choosing a particular later moment.

Practice exercises

Exercise 1 · Warm-upPredict the live-key value

Read the four lines and type the value printed 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 an ephemeron mark

    Use the model to predict the two comma-separated marked names.

    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 · PracticeBreak the circular rescue

      What does the model print when a value points back to a key but no root reaches the key?

      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 · PracticeName the later weak read

        After a later task and collection, what can deref() return instead of an object?

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

          Exercise 5 · ChallengeChoose required cleanup

          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 · ChallengeApply WeakMap to a real website

            A profile list owns user objects and a UI helper needs optional row metadata. What should the helper use as its weak key?

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

              Check your understanding

              For each question, separate ordinary ownership, conditional WeakMap marking, optional timing, and the narrow current-job guarantee.

              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 a normal object edge?

                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 can this print at a later time?

                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 required cleanup be handled?

                Choose an answer to see the explanation.

              6. Question 6 of 6What does the current job include for this rule?

                Choose an answer to see the explanation.

              Key takeaways

              • Strong edges preserve targets; weak edges can observe targets without owning them.
              • A WeakMap behaves like ephemerons: a marked key unlocks its value during a repeated marking loop.
              • A value that points back to a dead key cannot make that entry live by itself.
              • WeakRef can return undefined later, and finalizers are never a required cleanup mechanism.
              • Creating or reading a WeakRef keeps its target available through the current job and its promise jobs.
              • Use weak APIs for optional associations, not to avoid ordinary ownership or cache policy.

              Remember the one-liner.
              Weak APIs can observe or associate objects without becoming the reason those objects stay alive.

              Coming next: Garbage collection across engines & the DOM.

              CompleteFrontend Clear concepts. Working examples.