Which @angular/* package(s) are the source of the bug?
core
Is this a regression?
No
Description
Calling ApplicationRef.tick() from outside the Angular zone reports a
spurious NG0101: ApplicationRef.tick is called recursively, even though the
app never calls tick() recursively. Running work outside the zone and
ticking manually is a normal performance pattern.
During that tick Angular re-enters the Angular zone by itself. Effects capture
Zone.current when created, so both ZoneAwareEffectScheduler.flush() and
runEffectsInView() run them via zone.run(...). Because the tick started
outside the zone, leaving that zone.run drops NgZone._nesting back to 0,
so checkStable emits onMicrotaskEmpty synchronously in the middle of the
running tick.
NgZoneChangeDetectionScheduler's handler only guards on
changeDetectionScheduler.runningTick, which is true only for ticks started
by ChangeDetectionSchedulerImpl. For an explicit ApplicationRef.tick() it
is false, so the handler calls _tick() again and tickImpl throws.
Compare ChangeDetectionSchedulerImpl.shouldScheduleTick(), which checks both
this.runningTick and this.appRef._runningTick.
The error is caught and routed to the ErrorHandler, so nothing actually
breaks: ViewTreeGlobal is set before the throw and rendering is unaffected.
It is pure noise. In production the message is stripped and the stack is
almost entirely zone.js frames, so it is untraceable. We have been getting
these in Rollbar from a large zone-based app and could not identify a culprit.
Please provide a link to a minimal reproduction of the bug
https://github.com/arturovt/ng0101-recursive-tick-repro
Please provide the exception or error you saw
ERROR RuntimeError: NG0101: ApplicationRef.tick is called recursively
Please provide the environment you discovered this bug in (run ng version)
Anything else?
The setTimeout in the reproduction is load-bearing. A click handler runs as
a task of the Angular zone, so _nesting stays above 0 and checkStable
never fires. Scheduling the timer outside the zone makes the callback run at
nesting 0, which is where a real app sits when it ticks from a rAF, an
unpatched timer or a third party callback.
Both effect paths reproduce: root effects via EffectScheduler.flush() and
view effects via runEffectsInView().
I have a fix ready and will open a PR: also check applicationRef._runningTick
in the onMicrotaskEmpty handler, and set ViewTreeGlobal instead of starting
a nested tick. That is exactly what the current code does immediately before it
throws, so rendering behaviour is unchanged.
Which @angular/* package(s) are the source of the bug?
core
Is this a regression?
No
Description
Calling
ApplicationRef.tick()from outside the Angular zone reports aspurious
NG0101: ApplicationRef.tick is called recursively, even though theapp never calls
tick()recursively. Running work outside the zone andticking manually is a normal performance pattern.
During that tick Angular re-enters the Angular zone by itself. Effects capture
Zone.currentwhen created, so bothZoneAwareEffectScheduler.flush()andrunEffectsInView()run them viazone.run(...). Because the tick startedoutside the zone, leaving that
zone.rundropsNgZone._nestingback to 0,so
checkStableemitsonMicrotaskEmptysynchronously in the middle of therunning tick.
NgZoneChangeDetectionScheduler's handler only guards onchangeDetectionScheduler.runningTick, which is true only for ticks startedby
ChangeDetectionSchedulerImpl. For an explicitApplicationRef.tick()itis false, so the handler calls
_tick()again andtickImplthrows.Compare
ChangeDetectionSchedulerImpl.shouldScheduleTick(), which checks boththis.runningTickandthis.appRef._runningTick.The error is caught and routed to the
ErrorHandler, so nothing actuallybreaks:
ViewTreeGlobalis set before the throw and rendering is unaffected.It is pure noise. In production the message is stripped and the stack is
almost entirely zone.js frames, so it is untraceable. We have been getting
these in Rollbar from a large zone-based app and could not identify a culprit.
Please provide a link to a minimal reproduction of the bug
https://github.com/arturovt/ng0101-recursive-tick-repro
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run
ng version)Anything else?
The
setTimeoutin the reproduction is load-bearing. A click handler runs asa task of the Angular zone, so
_nestingstays above 0 andcheckStablenever fires. Scheduling the timer outside the zone makes the callback run at
nesting 0, which is where a real app sits when it ticks from a rAF, an
unpatched timer or a third party callback.
Both effect paths reproduce: root effects via
EffectScheduler.flush()andview effects via
runEffectsInView().I have a fix ready and will open a PR: also check
applicationRef._runningTickin the
onMicrotaskEmptyhandler, and setViewTreeGlobalinstead of startinga nested tick. That is exactly what the current code does immediately before it
throws, so rendering behaviour is unchanged.