Skip to content

fix(ext/node): bind setImmediate callback this to the Immediate instance#33716

Merged
bartlomieju merged 1 commit intodenoland:mainfrom
nathanwhitbot:fix/node-compat-iter75
Apr 30, 2026
Merged

fix(ext/node): bind setImmediate callback this to the Immediate instance#33716
bartlomieju merged 1 commit intodenoland:mainfrom
nathanwhitbot:fix/node-compat-iter75

Conversation

@nathanwhitbot
Copy link
Copy Markdown
Contributor

Summary

The Immediate class in internal/timers.mjs invoked the user callback with ReflectApply(unboundCallback, globalThis, argv), so setImmediate(function () { this }) saw Window / globalThis. Node's lib/internal/timers.js calls immediate._onImmediate(...argv) (method call), so this is the Immediate instance.

Fix: capture this in the constructor (before defining the inner closure) and pass it to ReflectApply. Timeout already does this correctly via its invokeCallback (ReflectApply(currentCb, self, args)), so setTimeout / setInterval were unaffected — only setImmediate was wrong.

Enables parallel/test-timers-this.js (which asserts this === handler for all of setTimeout / setInterval / setImmediate).

Test plan

  • cargo test --test node_compat -- test-timers-this passes.
  • Full cargo test --test node_compat on Linux — only pre-existing flaky failures (test-process-threadCpuUsage-worker-threads.js, test-fs-promises-file-handle-readFile.js, test-dgram-send-cb-quelches-error.js, test-child-process-uid-gid.js, test-net-autoselectfamily.js); no regressions in any of the existing setImmediate tests.

@nathanwhitbot nathanwhitbot force-pushed the fix/node-compat-iter75 branch from 0de5876 to 1db3f80 Compare April 29, 2026 20:58
@nathanwhitbot
Copy link
Copy Markdown
Contributor Author

Force-pushed 1db3f80a6 — branch was stacked on #33653. Reset to upstream/main and cherry-picked just the timers fix. Diff is now 2 files, +5 -1.

Copy link
Copy Markdown
Contributor Author

@nathanwhitbot nathanwhitbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review of `1db3f80a6`:

`Immediate` callback `this` parity with Node (`lib/internal/timers.js:498-507`):

Node's invocation site (line 504/506):
```js
if (!argv)
immediate._onImmediate();
else
immediate._onImmediate(...argv);
```

This is a method call — `this === immediate`. Our previous Deno polyfill used:
```js
return ReflectApply(unboundCallback, globalThis, argv);
```
which set `this === globalThis` instead. Per Node, `setImmediate(function () { this })` should observe the Immediate instance, not the global.

Fix captures `this` in the constructor (before defining the inner closure) and passes it through:
```js
const self = this;
const callback = (...argv) => {
...
return ReflectApply(unboundCallback, self, argv);
};
```
✓ matches Node's observable behavior.

Why `Timeout` was already correct:
`Timeout`'s `invokeCallback` already does `ReflectApply(currentCb, self, args)` where `self` is the Timeout instance, so `setTimeout`/`setInterval` were unaffected — only the `Immediate` path was wrong. The arrow-function callback in `Immediate` shadowed `this` lexically (resolving to outer scope = module scope = `undefined`/`globalThis` in strict mode), which is why the constructor-level `const self = this` capture is needed.

Test enrollment: `parallel/test-timers-this.js` is alphabetically positioned between `test-timers-socket-timeout-removes-other-socket-unref-timer.js` and `test-timers-throw-when-cb-not-function.js`. The test asserts `this === handler` for all three of `setTimeout` / `setInterval` / `setImmediate`. ✓

LGTM, holding to COMMENT until CI completes.

Copy link
Copy Markdown
Contributor Author

@nathanwhitbot nathanwhitbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single-commit, minimal diff. Confirmed Node behavior at lib/internal/timers.js#L502-506 — the dispatcher calls immediate._onImmediate(...argv) as a method, so this is the Immediate instance, not globalThis. PR's const self = this; ReflectApply(unboundCallback, self, argv) produces the matching binding. LGTM.

Copy link
Copy Markdown
Contributor

@fibibot fibibot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Substance is right. Verified against Node's lib/internal/timers.js:

immediate._onImmediate(...argv);   // method call → `this === immediate`

Pre-PR Deno: ReflectApply(unboundCallback, globalThis, argv) (globalThis as this). Post-PR: captures this into self in the Immediate constructor and passes it to ReflectApply, matching Node's method-call semantics. ✓

The closure-capture pattern (const self = this; before defining the inner arrow function) is the standard idiom and is already what Timeout.invokeCallback (cited in the body) does, so the fix brings setImmediate in line with the existing setTimeout / setInterval behavior.

Test enrollment alphabetically positioned correctly (test-timers-socket-timeout-removes-other-socket-unref-timer < test-timers-this < test-timers-throw-when-cb-not-function). ✓

CI: 80 success, 0 failure, 24 still pending. Holding to COMMENT per the CI-must-complete rule. Substance is a 5-line one-liner; happy to flip to APPROVE on the next iteration once the relevant test node_compat debug shards land green.

@bartlomieju bartlomieju merged commit 1e6ce8d into denoland:main Apr 30, 2026
136 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants