Skip to content

fix(core): skip nested tick from onMicrotaskEmpty while tick is running - #70985

Open
arturovt wants to merge 1 commit into
angular:mainfrom
arturovt:fix/zone-scheduler-recursive-tick
Open

arturovt wants to merge 1 commit into
angular:mainfrom
arturovt:fix/zone-scheduler-recursive-tick

Conversation

@arturovt

@arturovt arturovt commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

A zone-based app that calls ApplicationRef.tick() from outside the Angular
zone can report a spurious NG0101 ("ApplicationRef.tick is called
recursively"). Running work outside the zone and ticking manually is a normal
performance pattern, so the app is correct. In production the error carries no
message and the stack is almost entirely zone.js frames, which makes it
impossible to trace back to the real caller.

Angular enters the Angular zone by itself during such a tick. Effects remember
the zone they were created in, so both the root effect scheduler and
runEffectsInView use zone.run() to run them. Leaving that zone.run()
drops the nesting count back to zero, because the tick started outside the
zone. checkStable then emits onMicrotaskEmpty synchronously, in the middle
of the running tick.

The zone scheduler's guard for this only checked the zoneless scheduler's
runningTick flag. That flag is false for an explicit tick, so the handler
started a nested _tick(), which threw. The guard was added in #55290 and its
comment already describes the mechanism, but it only covered ticks the
scheduler itself had started.

Check ApplicationRef._runningTick as well and return, the same way the
handler already bails out for a scheduled tick. The running tick needs no help
from the handler: its first pass in a zone app is already global, and if an
effect dirties a view mid-tick, syncDirtyFlagsWithViews() loops back for
another pass on its own. Rendering behaviour does not change, only the false
error goes away.

Added tests for both effect paths that fail without the fix.

Fixes #70984

@pullapprove
pullapprove Bot requested a review from JeanMeche September 27, 2026 12:07
@angular-robot angular-robot Bot added the area: core Issues related to the framework runtime label Sep 27, 2026
@ngbot ngbot Bot added this to the Backlog milestone Sep 27, 2026
@JeanMeche
JeanMeche requested review from atscott and removed request for JeanMeche September 27, 2026 17:46
Comment thread packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts
…ning

A zone-based app that calls `ApplicationRef.tick()` from outside the Angular
zone can report a spurious NG0101 ("ApplicationRef.tick is called
recursively"). Running work outside the zone and ticking manually is a normal
performance pattern, so the app is correct. In production the error carries no
message and the stack is almost entirely zone.js frames, which makes it
impossible to trace back to the real caller.

Angular enters the Angular zone by itself during such a tick. Effects remember
the zone they were created in, so both the root effect scheduler and
`runEffectsInView` use `zone.run()` to run them. Leaving that `zone.run()`
drops the nesting count back to zero, because the tick started outside the
zone. `checkStable` then emits `onMicrotaskEmpty` synchronously, in the middle
of the running tick.

The zone scheduler's guard for this only checked the zoneless scheduler's
`runningTick` flag. That flag is false for an explicit tick, so the handler
started a nested `_tick()`, which threw. The guard was added in angular#55290 and its
comment already describes the mechanism, but it only covered ticks the
scheduler itself had started.

Check `ApplicationRef._runningTick` as well and return, the same way the
handler already bails out for a scheduled tick. The running tick needs no help
from the handler: its first pass in a zone app is already global, and if an
effect dirties a view mid-tick, `syncDirtyFlagsWithViews()` loops back for
another pass on its own. Rendering behaviour does not change, only the false
error goes away.

Added tests for both effect paths that fail without the fix.

Fixes angular#70984
@arturovt
arturovt force-pushed the fix/zone-scheduler-recursive-tick branch from c6caeb4 to b8dd973 Compare September 28, 2026 18:50
@arturovt
arturovt requested a review from atscott September 28, 2026 18:51
@atscott atscott added requires: TGP This PR requires a passing TGP before merging is allowed target: patch This PR is targeted for the next patch release labels Sep 28, 2026
@JeanMeche

Copy link
Copy Markdown
Member

Started TGP

@JeanMeche

Copy link
Copy Markdown
Member

TGP is "green" (all breakages/failures are unrelated)

@JeanMeche JeanMeche removed the requires: TGP This PR requires a passing TGP before merging is allowed label Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Issues related to the framework runtime target: patch This PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spurious NG0101 when ApplicationRef.tick() is called outside the Angular zone and an effect is flushed

3 participants