Skip to content

UpgradeModule: Exception in $digest breaks change propagation #24680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
steckerl opened this issue Jun 26, 2018 · 29 comments
Closed

UpgradeModule: Exception in $digest breaks change propagation #24680

steckerl opened this issue Jun 26, 2018 · 29 comments
Labels
area: upgrade Issues related to AngularJS → Angular upgrade APIs freq1: low target: patch This PR is targeted for the next patch release
Milestone

Comments

@steckerl
Copy link

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report  
[ ] Performance issue
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
[ ] Other... Please describe:

Current behavior

When $rootScope.$digest throws an exception ($digest already in progress), change propagation stops working for downgraded components.

That is because the exception breaks the subscription on onMicrotaskEmpty here:

https://github.com/angular/angular/blob/master/packages/upgrade/src/static/upgrade_module.ts#L244

setTimeout(() => {
  const $rootScope = $injector.get('$rootScope');
  const subscription = this.ngZone.onMicrotaskEmpty.subscribe(() => $rootScope.$digest());
  $rootScope.$on('$destroy', () => { subscription.unsubscribe(); });
}, 0);

Expected behavior

UpgradeModule should be more fault tolerant (try-catch?)
or
Use a different pattern. (Check if digest already running?)

Minimal reproduction of the problem with instructions

I get this "$digest already in progress" almost every time I open an angularjs material dialog or a sidenav.

What is the motivation / use case for changing the behavior?

Environment


Angular version: 6.0.X


Browser:
- [x] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 
@splincode
Copy link
Contributor

please, add repo for reproduce

@ngbot ngbot bot added this to the needsTriage milestone Jun 26, 2018
@jasonaden jasonaden added state: Follow Up needs reproduction This issue needs a reproduction in order for the team to investigate further and removed state: Follow Up labels Jun 26, 2018
@jasonaden
Copy link
Contributor

I'm sorry but reported issues require a reproduction of the issue. We suggest using StackBlitz, Plunker or Github.

If this issue persists, please create a reproduction using one of the links above. Describe the difference between the expected and current behavior in a new issue with the reproduction linked.

@gkalpak
Copy link
Member

gkalpak commented Jun 27, 2018

For ngUpgrade, you can use this StackBlitz project as a starting point for creating a reproduction.

@steckerl
Copy link
Author

Thank you for the starting point, I just spent the whole day setting up a demo but I just cannot repro the issue.
So I digged further in my StackTrace and found out that focus() is called within a digest circle, which emits 'blur' and 'focus' events, which leads to another $digest due to the subscription above.

beginPhase (vendor.js:76783)
$digest (vendor.js:76113)
(anonymous) (static.js:1475)
schedulerFn (core.js:3397)
push.../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.__tryOrUnsub (Subscriber.js:195)
push.../node_modules/rxjs/_esm5/internal/Subscriber.js.SafeSubscriber.next (Subscriber.js:133)
push.../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._next (Subscriber.js:77)
push.../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next (Subscriber.js:54)
push.../node_modules/rxjs/_esm5/internal/Subject.js.Subject.next (Subject.js:47)
push.../node_modules/@angular/core/fesm5/core.js.EventEmitter.emit (core.js:3377)
checkStable (core.js:3623)
onLeave (core.js:3690)
onInvokeTask (core.js:3648)
push.../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:420)
push.../node_modules/zone.js/dist/zone.js.Zone.runTask (zone.js:188)
push.../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask (zone.js:496)
invokeTask (zone.js:1540)
globalZoneAwareCallback (zone.js:1566)
focusOnOpen (vendor.js:28239)
(anonymous) (vendor.js:28220)
processQueue (vendor.js:74976)
(anonymous) (vendor.js:75024)
$digest (vendor.js:76137)
(anonymous) (vendor.js:76457)
completeOutstandingRequest (vendor.js:64081)
(anonymous) (vendor.js:64359)
push.../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:421)
push.../node_modules/zone.js/dist/zone.js.Zone.runTask (zone.js:188)
push.../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask (zone.js:496)
ZoneTask.invoke (zone.js:485)
timer (zone.js:2054)

My temporary solution is to blacklist focus and blur in zone.js:

__zone_symbol__BLACK_LISTED_EVENTS = ['blur', 'focus']

But:

I still think the code within the subscription is just wrong. An exception within $digest should not leave UpgradeModule in a faulty state.

@gkalpak
Copy link
Member

gkalpak commented Jun 27, 2018

Generally, $digest() handles errors in watchers internally, so an error there will not throw the error. But $digest in progress errors are not handled, so they bubble up and break the subscription.
(That said, I wouldn't necessarily be opposed to explicitly wrapping $rootScope.$digest() in a try/catch.)

Now that you've identified the source of the issue, are you able to create a reproduction?
If it is a valid usecase, we may decide to use a "safe apply".

@steckerl
Copy link
Author

Here it is:

https://ngupgradestatic-playground-26mk87.stackblitz.io/

Counter stops refreshing after button is clicked.

It is somehow related to __Zone_disable_requestAnimationFrame = true; (but this really speeds up my hybrid app.)

Do I need to open another issue?

@gkalpak gkalpak reopened this Jun 27, 2018
@gkalpak
Copy link
Member

gkalpak commented Jun 28, 2018

Interesting usecase 😕

@gkalpak gkalpak added freq1: low target: patch This PR is targeted for the next patch release and removed needs reproduction This issue needs a reproduction in order for the team to investigate further labels Jun 28, 2018
@JiaLiPassion
Copy link
Contributor

JiaLiPassion commented Jul 7, 2018

I think this is an issue that angular material conflict with zone.js in upgrade/downgrade case.

  1. button clicked.-> ngZone
  2. angular material open dialog with an animation -> setTimeout -> ngZone -> digest
  3. in the animation handler it will focus the dialog -> onFocus -> ngZone -> digest again.

Then ERROR Error: [$rootScope:inprog] $digest already in progress error will occur, and the counter stop refresh.

To resolve the issue now, you can add the following lines after

__zone_symbol__BLACK_LISTED_EVENTS = ['transitionend', 'focus', 'pointermove', 'mousemove', 'pointerdown', 'mouseout', 'blur'];

The focus and blur is to resolve this issue, and the others are to improve performance.

Updated, @steckerl , oh, you have found the solution, I didn't read your comment.

here is a working sample, https://stackblitz.com/edit/ngupgradestatic-playground-rbww6n?file=index.html

@gkalpak, I think in the most cases, those mousemove, transition, pointer event handler registered by Angular Material will not need to be in the zone, should I put it into document?

@gkalpak
Copy link
Member

gkalpak commented Jul 7, 2018

Thx for looking into this @JiaLiPassion. As far as I understand, this is a general Angular Material consideration, not specific to ngUpgrade, right? If so, maybe the Angular Material docs are a more suitable place. Maybe you could open an issue or submit a PR on their repo.

(As far as ngUpgrade is concerned, I think the user should not have to worry about these things causing "digest in progress" errors, so I am still in favor of using a "safe apply" onMicrotaskEmpty for ngUpgrade.)

@JiaLiPassion
Copy link
Contributor

@gkalpak, yes, in fact, I am also wondering this is not an angular material issue, my solution is just a walk around. I will do more research and submit PR in angular material.

I have a question, in this case, should we use onMicrotaskEmpty in ngUpgrade?
I think the case can be simplified as the following code.

// angular1 component
elem.addEventListener('click', clickHandler);
elem.addEventListener('focus', focusHandler);
const clickHandler = function() {
  elem.focus();
};
const focusHandler = function() {};

such code in ngUpgrade may cause the same issue, I will create a reproduce later.
I just want to confirm that should addEventListener/setTimeout/Promise.then in angular1 also run in ngZone?

@gkalpak
Copy link
Member

gkalpak commented Jul 7, 2018

With ngUpgrade, the AngularJS part of the app is run inside the Angular zone. This way, every async event that happens in the AngularJS part of the app will cause CD in Angular.

For the same reasons, we call $digest() (AngularJS's CD) onMicrotaskEmpty, so that every time CD is triggered in Angular, it is also triggered in AngularJS.

@JiaLiPassion
Copy link
Contributor

@gkalpak, thank you, that makes sense.

  • Angular 1 async -> ngZone -> Angular2 CD
  • Angular 2 ngZone -> onMicrotaskEmpty -> Angular 1 $digest()

Such kind of single direction thing will work as expected.

And in this case.

  • Angular 1 eventHandler -> ngZone -> Angular 2 onMicrotaskEmpty -> Angular1 $digest()

So Angular 1 will call $digest() because Angular 1 itself not because Angular 2.
Is this also by design? I think Angular1 should only call $digest() if the onMicrotaskEmpty was triggered only by Angular 2 world.

@gkalpak
Copy link
Member

gkalpak commented Jul 9, 2018

I think Angular1 should only call $digest() if the onMicrotaskEmpty was triggered only by Angular 2 world.

Yes, but I guess it is not easy to know what really triggered the onMicrotaskEmpty.

@JiaLiPassion
Copy link
Contributor

@gkalpak, maybe we can create a special zone in Angular 1.x when $digest.

ngZone.onMicrotaskEmpty.subscribe(() => {
  // already in digestZone, just return
  if (Zone.current.get('digestZone')) {
    return;
  }
  digestZone.run(() => {
    $digest(); 
  });
});

I will write a sample to test it.

@llRandom
Copy link

llRandom commented Aug 6, 2018

The same problem happens with upgraded components that use angular-ui components, so it's not only angular-material. Same problem, element[0].focus() call triggers the first $digest, and the second triggered by zone.js

@andrewrheingans
Copy link

andrewrheingans commented Aug 27, 2018

I ran into this problem as well. I tracked down the source of the $digest in progress error and fixed it (we were outside the angular zone and then ngZone.run was called inside an $apply which ran a $digest synchronously) but regardless of that, this issue still makes the app way too fickle in my opinion. If the user runs into a single $digest in progress error (and those seem to live in the hidden cracks of the app :) ), then the app is half dead until the user refreshes the page. That's not going to work for us, so for now I'm just going to monkey patch $digest and wrap it in a try/catch (using the angular1 decorator setup). Is there any reason not to wrap the onMicrotaskEmpty $digest in a try/catch? If not, is there any way I can help - put together a PR or something?

@gkalpak
Copy link
Member

gkalpak commented Aug 27, 2018

Wrapping $rootScope.$digest() in try...catch sgtm. @andrewrheingans, if you are up to submitting a PR, tht would be awesome ❤️

@andrewrheingans
Copy link

Sounds good I'll try to get a PR ready tomorrow!

@andrewrheingans
Copy link

Sorry I haven't gotten to this yet! Hope to do it soon

hisabimbola pushed a commit to hisabimbola/angular that referenced this issue Jan 24, 2019

Unverified

No user is associated with the committer email.
Closes angular#24680
@hisabimbola
Copy link

Hey here,

I am having this issue too and have submitted a PR #28337 to catch the exception as suggested by @gkalpak

I am not sure what to do with the error in the try..catch. Now I just log it out. Any better way to handle this is welcome.

Also, I was stuck with the test, any help how to write the test for this will be appreciated

@gkalpak gkalpak added area: upgrade Issues related to AngularJS → Angular upgrade APIs and removed area: upgrade Issues related to AngularJS → Angular upgrade APIs comp: upgrade/static labels Mar 13, 2019
@dblVs
Copy link

dblVs commented Mar 15, 2019

@gkalpak I've looked into a fix. I tried to recreate the issue, but i cannot create a minimal steps to reproduce the bug. It seems to be something to do with running a $digest outside angular, and that $digest inside has a task that initiates synchronously zone.js task (focus) and once the task queue is empty zone.js runs the $digest while there is another $digest running.

I do not think that checking the $$phase before executing the $digest is correct, but i have yet to figure out EXACTLY what causes it. In every case that i've seen the bug it has something to do with ngAnimate. I'm suspecting it's to do with the requestAnimationFrame inside it.

I have 2 places in my app where thats happening and both places are to do with ngAnimate and the stackblitz provided uses ngAnimate as well.

I've created a build with the $$phase fix and when i need a $digest it wasn't happening. I had a button with binding and it was not updating.

If some can create repro steps with minimal code and no 3rd party libraries, I will create a PR.

@gkalpak
Copy link
Member

gkalpak commented Mar 16, 2019

Thx for looking into it, @dblVs. We are already discussing this on PR #28337, so let's move the discussion there.

@MikeMatusz
Copy link

MikeMatusz commented Mar 20, 2019

I'm also running into this issue. A third-party AngularJS component (ui-grid) is handling $document.on('mouseup', handler) and then calling $timeout() from the handler, which runs outside the Angular zone because it's a jQuery event, and results in broken change detection. Looks like a lot of people are actively working on the PR, but I'd be happy to lend a hand if needed. I've confirmed that the proposed changes do fix my issue locally.

@dblVs
Copy link

dblVs commented Mar 20, 2019

@MikeMatusz I've kinda been stuck on trying to make "real life" case for the tests, because I believe that If you test the implementation that test case is useless

@MikeMatusz
Copy link

@dblVs I'll take a look at your StackBlitz today. Also, in my case, removing ngAnimate doesn't make a difference, so I don't think it's exclusive to that, though it may be a common case.

@gkalpak What can we do from AngularJS code to ensure $digests are called from the Angular zone? As a temporary workaround, I'm downgrading NgZone and calling .run() in the couple places that are causing problems for me, but it seems like a flaw in UpgradeModule for that to be necessary. Would it make sense for $scope.$apply and $timeout to be patched to ensure they're running in the Angular zone if they're called from outside of it?

@gkalpak
Copy link
Member

gkalpak commented Mar 21, 2019

@MikeMatusz, I don't think it matters. Why do you want to always run $digest() from inside the Angular zone? This also doesn't sound related to this issue.

In any case, if anyone is looking for a (quick and dirty) work around for this issue, you could monkeypatch $rootScope.$digest() to not run if a $digest()/$apply() is already in progress. E.g.:

angular.
  module('myApp').
  decorator('$rootScope', $delegate => {
    const originalDigest = $delegate.$digest;
    $delegate.$digest = () => {
      if (!$delegate.$$phase) {
        originalDigest.call($delegate);
      }
    };
    return $delegate;
  });

@MikeMatusz
Copy link

@gkalpak Based on the zone discussion at the top of this thread, and the comments on #18979 which resulted in the same error, it seems like a reasonable question. The guidance on 18979 was to make sure that AngularJS was bootstrapped in the NgZone. When $digest is called in the NgZone, I don't think this error is possible, so I was just wondering if there was a proactive approach possible to preventing the error instead of just try/catch/continue.

@gkalpak
Copy link
Member

gkalpak commented Mar 25, 2019

The error is possible even when bootstrapping AngularJS inside the Angular zone. So, ensuring the bootstrapping happens inside the Angular zone does not eliminate the error.
(We could add a check anyway, but I am afraid this could potentially break people intentionally doing this. In any case, it would be a breaking change, so can't land before 9.0.)

samjulien added a commit to samjulien/angular that referenced this issue Apr 23, 2019
…is in progress

Fixes angular#24680, where onMicrotaskEmpty was triggering an extra $digest in both dynamic UpgradeAdapter and static UpgradeModule.
gkalpak pushed a commit to samjulien/angular that referenced this issue Apr 24, 2019

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
…st` is in progress

Previously, under certain circumstances, `NgZone#onMicrotaskEmpty` could
emit while a `$digest` was in progress, thus triggering another
`$digest`, which in turn would throw a `$digest already in progress`
error. Furthermore, throwing an error from inside the `onMicrotaskEmpty`
subscription would result in unsubscribing and stop triggering further
`$digest`s, when `onMicrotaskEmpty` emitted.

Usually, emitting while a `$digest` was already in progress was a result
of unintentionally running some part of AngularJS outside the Angular
zone, but there are valid (if rare) usecases where this can happen
(see angular#24680 for details).

This commit addresses the issue as follows:
- If a `$digest` is in progress when `onMicrotaskEmpty` emits, do not
  trigger another `$digest` (to avoid the error). `$evalAsync()` is used
  instead, to ensure that the bindings are evaluated at least once more.
- Since there is still a high probability that the situation is a result
  of programming error (i.e. some AngularJS part running outside the
  Angular Zone), a warning will be logged, but only if the app is in
  [dev mode][1].

[1]: https://github.com/angular/angular/blob/78146c189/packages/core/src/util/ng_dev_mode.ts#L12

Fixes angular#24680
gkalpak pushed a commit to gkalpak/angular that referenced this issue Apr 25, 2019

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
…st` is in progress (angular#29794)

Previously, under certain circumstances, `NgZone#onMicrotaskEmpty` could
emit while a `$digest` was in progress, thus triggering another
`$digest`, which in turn would throw a `$digest already in progress`
error. Furthermore, throwing an error from inside the `onMicrotaskEmpty`
subscription would result in unsubscribing and stop triggering further
`$digest`s, when `onMicrotaskEmpty` emitted.

Usually, emitting while a `$digest` was already in progress was a result
of unintentionally running some part of AngularJS outside the Angular
zone, but there are valid (if rare) usecases where this can happen
(see angular#24680 for details).

This commit addresses the issue as follows:
- If a `$digest` is in progress when `onMicrotaskEmpty` emits, do not
  trigger another `$digest` (to avoid the error). `$evalAsync()` is used
  instead, to ensure that the bindings are evaluated at least once more.
- Since there is still a high probability that the situation is a result
  of programming error (i.e. some AngularJS part running outside the
  Angular Zone), a warning will be logged, but only if the app is in
  [dev mode][1].

[1]: https://github.com/angular/angular/blob/78146c189/packages/core/src/util/ng_dev_mode.ts#L12

Fixes angular#24680

PR Close angular#29794
AndrewKushnir pushed a commit that referenced this issue Apr 25, 2019
…st` is in progress (#29794) (#30107)

Previously, under certain circumstances, `NgZone#onMicrotaskEmpty` could
emit while a `$digest` was in progress, thus triggering another
`$digest`, which in turn would throw a `$digest already in progress`
error. Furthermore, throwing an error from inside the `onMicrotaskEmpty`
subscription would result in unsubscribing and stop triggering further
`$digest`s, when `onMicrotaskEmpty` emitted.

Usually, emitting while a `$digest` was already in progress was a result
of unintentionally running some part of AngularJS outside the Angular
zone, but there are valid (if rare) usecases where this can happen
(see #24680 for details).

This commit addresses the issue as follows:
- If a `$digest` is in progress when `onMicrotaskEmpty` emits, do not
  trigger another `$digest` (to avoid the error). `$evalAsync()` is used
  instead, to ensure that the bindings are evaluated at least once more.
- Since there is still a high probability that the situation is a result
  of programming error (i.e. some AngularJS part running outside the
  Angular Zone), a warning will be logged, but only if the app is in
  [dev mode][1].

[1]: https://github.com/angular/angular/blob/78146c189/packages/core/src/util/ng_dev_mode.ts#L12

Fixes #24680

PR Close #29794

PR Close #30107
BioPhoton pushed a commit to BioPhoton/angular that referenced this issue May 21, 2019
…st` is in progress (angular#29794)

Previously, under certain circumstances, `NgZone#onMicrotaskEmpty` could
emit while a `$digest` was in progress, thus triggering another
`$digest`, which in turn would throw a `$digest already in progress`
error. Furthermore, throwing an error from inside the `onMicrotaskEmpty`
subscription would result in unsubscribing and stop triggering further
`$digest`s, when `onMicrotaskEmpty` emitted.

Usually, emitting while a `$digest` was already in progress was a result
of unintentionally running some part of AngularJS outside the Angular
zone, but there are valid (if rare) usecases where this can happen
(see angular#24680 for details).

This commit addresses the issue as follows:
- If a `$digest` is in progress when `onMicrotaskEmpty` emits, do not
  trigger another `$digest` (to avoid the error). `$evalAsync()` is used
  instead, to ensure that the bindings are evaluated at least once more.
- Since there is still a high probability that the situation is a result
  of programming error (i.e. some AngularJS part running outside the
  Angular Zone), a warning will be logged, but only if the app is in
  [dev mode][1].

[1]: https://github.com/angular/angular/blob/78146c189/packages/core/src/util/ng_dev_mode.ts#L12

Fixes angular#24680

PR Close angular#29794
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 15, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: upgrade Issues related to AngularJS → Angular upgrade APIs freq1: low target: patch This PR is targeted for the next patch release
Projects
None yet
10 participants