Conversation
atscott
reviewed
Sep 28, 2026
…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
force-pushed
the
fix/zone-scheduler-recursive-tick
branch
from
September 28, 2026 18:50
c6caeb4 to
b8dd973
Compare
Member
Member
|
TGP is "green" (all breakages/failures are unrelated) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A zone-based app that calls
ApplicationRef.tick()from outside the Angularzone 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
runEffectsInViewusezone.run()to run them. Leaving thatzone.run()drops the nesting count back to zero, because the tick started outside the
zone.
checkStablethen emitsonMicrotaskEmptysynchronously, in the middleof the running tick.
The zone scheduler's guard for this only checked the zoneless scheduler's
runningTickflag. That flag is false for an explicit tick, so the handlerstarted a nested
_tick(), which threw. The guard was added in #55290 and itscomment already describes the mechanism, but it only covered ticks the
scheduler itself had started.
Check
ApplicationRef._runningTickas well and return, the same way thehandler 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 foranother 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