Skip to content

fix(testing): run microtasks between timers in FakeTime async advancement - #7310

Open
tomas-zijdemans wants to merge 2 commits into
denoland:mainfrom
tomas-zijdemans:fix-fake-time-async-checkpoints
Open

fix(testing): run microtasks between timers in FakeTime async advancement#7310
tomas-zijdemans wants to merge 2 commits into
denoland:mainfrom
tomas-zijdemans:fix-fake-time-async-checkpoints

Conversation

@tomas-zijdemans

Copy link
Copy Markdown
Contributor

tickAsync(), nextAsync() and runAllAsync() now drain microtasks after every timer callback, so a promise continuation created by a timer at 10 ms sees the clock at 10 ms instead of at the tick target.

Problem

tickAsync(ms) drained microtasks once, then handed off to the synchronous now setter, which ran every due callback in one loop with no microtask checkpoint in between. Three consequences, each reproduced in the new tests:

  • A timer at 10 ms that awaits a resolved promise and then schedules a 10 ms timeout ends up due at 30 ms. It should be due at 20 ms and should already have fired after tickAsync(20).
  • Two timers sharing a deadline ran as a batch, so runAllAsync() produced a, b, microtask while its docs promised microtasks before each timer.
  • runAllAsync() with an empty due tree returned immediately. A pending microtask that would have scheduled a timer never got its timer run.

Anything that chains await with timers (retry loops, debouncers, caches with TTL) was being tested against a schedule the runtime never produces.

Change

The now setter's loop body moves into runNextTimer(limit), which runs one live timer at or before limit and returns whether it did. The setter calls it in a loop, unchanged in behavior. The async paths share a private #advanceAsync(target): run one timer, drain microtasks, repeat until nothing is due at or before target, then set now = target. Timers scheduled during the loop that land inside the target take part. nextAsync() drains before checking the tree and advances to the next deadline through the same loop. runAllAsync() now loops on nextAsync() until it returns false.

Synchronous tick(), next(), runAll() and the now setter are untouched. FIFO order for equal deadlines is preserved. A callback's returned promise is not awaited, since it may depend on another timer in the same advancement.

Compatibility

This changes observable behavior of stable APIs. The old tickAsync() doc described the narrow behavior accurately, so callers could have relied on it. Two tests in async/retry_test.ts did: one asserted that the retry promise was still pending right after the final timer fired, the other attached its assertRejects handler after the advancement that rejects the promise, which now surfaces as an unhandled rejection. Both are updated. I think this is a fix since runAllAsync() already documented the per-timer contract and the old order can't occur under a real event loop, but happy to retitle if you'd rather treat it as breaking.

Tests

Nine new cases in testing/time_test.ts, seven of which fail on main:

  • timer, promise, timer chain observing the right clock
  • equal deadlines with an interleaved microtask
  • a microtask cancelling another due timer
  • a ten-deep await chain
  • a throwing callback stopping advancement with later timers left pending
  • both runAllAsync() gaps above

Lint, format check and the full test suite pass.

@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.04%. Comparing base (ca58f94) to head (8b978f5).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7310   +/-   ##
=======================================
  Coverage   95.03%   95.04%           
=======================================
  Files         617      618    +1     
  Lines       51637    51656   +19     
  Branches     9359     9363    +4     
=======================================
+ Hits        49075    49094   +19     
  Misses       2021     2021           
  Partials      541      541           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant