Skip to content

WeakRef Preventing Garbage Collection of a Null Object #61868

@Emil-Frisk

Description

@Emil-Frisk

Version

v24.13.1&v25.6.1

Platform

uname -a --> (Linux tihi 6.17.0-14-generic #14~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Jan 15 15:52:10 UTC 2 x86_64 x86_64 x86_64 GNU/Linux)

lsb_release -a -> (
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.4 LTS
Release:        24.04
Codename:       noble
)

Subsystem

No response

What steps will reproduce the bug?

Here is a test file that demonstrates the issue.

// WeakRef Bug Report - Node.js WeakRef prevents object GC -
// Tested with only sleeps and no manual GC calls same results.
// WeakRef is not acting as a weak reference
// Tested on: Feb 17th 2026 - Node versions v24.13.1 & v25.6.1
// Result: FAILED on both versions

const SLEEP_TIME = 5

const registry = new FinalizationRegistry((heldValue) => {
console.log(`Cleaned up resources for: ${heldValue}`);
});

const test1 = async () => {
  console.log("=== Test 1: WeakRef alone ===");
  let obj1 = { data: 'test1' };
  const obj1_ref = new WeakRef(obj1);
  console.log('Before null:', obj1_ref.deref() ? 'EXISTS' : 'undefined');
  obj1 = null;
  global.gc();
  
  await new Promise(resolve => {
    setTimeout(() => {
      console.log(`[Test 1] After ${SLEEP_TIME} s:`, obj1_ref.deref() ? 'Reference still EXISTS (BUG!)' : 'undefined (correct)');
      resolve();
    }, SLEEP_TIME*1000);
  });
}

const test2 = () => {
  console.log("\n=== Test 2: FinalizationRegistry alone ===");
  let obj2 = { data: 'test2' };
  registry.register(obj2, 'test2 object with no "weak" reference"');
  obj2 = null;
  global.gc();
}

const test3 = async ()  => {
    console.log("\n=== Test 3: Both together ===");
    let obj3 = { data: 'test3' };
    let obj4 = { data: 'test3 no weak reference' };
    const obj3_ref = new WeakRef(obj3);
    registry.register(obj3, 'test3 with "weak" reference');
    registry.register(obj4, 'test3 object with no "weak" reference');
    obj3 = null;
    obj4 = null;
    global.gc();
    await new Promise(resolve => {
    setTimeout(() => {
      console.log(`[Test 3] After ${SLEEP_TIME} s:`, obj3_ref.deref() ? 'Reference still EXISTS (BUG!)' : 'undefined (correct)');
      resolve();
    }, SLEEP_TIME*1000);
  });
}

async function report() {
  await test1()
  test2()
  await test3()
}

report()

### How often does it reproduce? Is there a required condition?

The bug reproduces each time. I tested the bug with different sleep timers, up to 240 seconds and there was no difference. WeakReference is preventing GC from working as expected. 

### What is the expected behavior? Why is that the expected behavior?

WeakRef should not prevent garbage collection. Many libraries depend on this, including on-exit-leak-free.

### What do you see instead?

=== Test 1: WeakRef alone ===
Before null: EXISTS
[Test 1] After 5 s: Reference still EXISTS (BUG!)

=== Test 2: FinalizationRegistry alone ===
Cleaned up resources for: test2 object with no "weak" reference"

=== Test 3: Both together ===
Cleaned up resources for: test3 object with no "weak" reference
[Test 3] After 5 s: Reference still EXISTS (BUG!)

### Additional information

I tested this without --expose-gc flag with only sleeps too and the result was the same. 

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions