Skip to content
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

ExpressionChangedAfterItHasBeenCheckedError is not thrown if ChangeDetection.OnPush is used #45612

Open
skrtheboss opened this issue Apr 13, 2022 · 5 comments
Labels
area: core Issues related to the framework runtime breaking changes core: change detection P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
Milestone

Comments

@skrtheboss
Copy link
Contributor

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

When changing a template value while the change detection is taking place, the user is warned with an ExpressionChangedAfterItHasBeenCheckedError.
This is not the case for components which use the ChangeDetectionStrategy.OnPush .

By doing some debugging I found the problem:

  1. The async pipe triggers a markForCheck for the component, while change detection is running.
  2. The refreshView clears the dirty state of the component after the view has been checked.
  3. The checkNoChanges is performed, and the refreshComponent function does not perform a refreshView for that component, since it is no longer dirty.

This results in no ExpressionChangedAfterItHasBeenCheckedError because the view is not checked, since its dirty status has been cleared in the change detection process

This can be really hard to debug without the warning. So I found a workaround for now, that is to add || isInCheckNoChangesMode() to the refreshView check so that it becomes if (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty) || isInCheckNoChangesMode()) {. This is very ugly and imperformant, but at least it helps detect those errors.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/angular-ivy-ked7hw?file=src/app/app.component.ts

Please provide the exception or error you saw

No response

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 13.3.2
Node: 16.14.2
Package Manager: yarn 1.22.18
OS: linux x64

Angular: 13.3.2
... animations, cdk, cdk-experimental, cli, common, compiler
... compiler-cli, core, forms, material, material-moment-adapter
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1303.2
@angular-devkit/build-angular   13.3.2
@angular-devkit/core            13.3.2
@angular-devkit/schematics      13.3.2
@schematics/angular             13.3.2
ng-packagr                      13.3.0
rxjs                            7.5.5
typescript                      4.6.3
webpack                         5.72.0

Anything else?

No response

@JoostK
Copy link
Member

JoostK commented Apr 13, 2022

Indeed, this is a known issue. It would be very breaky to enable this check for OnPush components though, which is why this hasn't been fixed as of now.

@JoostK JoostK added area: core Issues related to the framework runtime core: change detection P4 A relatively minor issue that is not relevant to core functions labels Apr 13, 2022
@ngbot ngbot bot modified the milestone: Backlog Apr 13, 2022
@atscott
Copy link
Contributor

atscott commented Apr 13, 2022

For reference, the fix was attempted in #34443 and was found to be massively breaky, as @JoostK mentioned.

@mirobo
Copy link

mirobo commented Jul 5, 2022

What is the progress on this issue? I found this issue only because I couldn't believe that suddenly ALL "ExpressionChangedAfterItHasBeenCheckedError" were fixed in our app after changing all components CDS to OnPush. All those errors just magically disappeared from our application... too good to be true :D

@alxhub alxhub added breaking changes P1 Impacts a large percentage of users; if a workaround exists it is partial or overly painful and removed P4 A relatively minor issue that is not relevant to core functions labels May 17, 2023
@SLKnutson
Copy link

As I understand it, this isn't just about the warning. The warning is telling the developer that the code isn't working the way they intended. In our case, we rearranged some code resulting in a fetch being triggered by an @input setter. Since the fetch was async, it still worked, and we didn't realize right away that we had lost the loading spinner.

It's interesting that calling checkNoChanges caused a lot of breakages. I wonder how many of those are really bugs. Can it at least be main opt-in?

Thanks!

@JoostK
Copy link
Member

JoostK commented May 31, 2023

I wonder how many of those are really bugs

All of them are bugs! The sad thing is that projects have actually used OnPush change detection to suppress ECAIHBC errors.

If this will be fixed this will indeed have to be opt-in, to avoid a large scale breaking change. We are reevaluating change detection details in support of the signal based components RFC, but there are not yet concrete plans on how OnPush behavior will be affected.

atscott added a commit to atscott/angular that referenced this issue Feb 8, 2024
…eing refreshed

This commit updates the internal `markViewDirty` logic to avoid marking
views dirty up to the root when we encounter a view that is already
being refreshed. This is wasteful because we clear the dirty bit at the
end of refreshing a view anyways so it really has no affect on that
view.

Sometimes we can reach a view and refresh it when the parents have been
skipped. For example, this happens with views that have updated signals.
Ancestors are only traversed and not refreshed. Marking dirty all the
way up to the root in this case can result in those ancestor views being
marked with the dirty flag and it not being cleared.

This only became problematic when we started to support backwards
referenced transplanted views and changed signal change detection to
not mark ancestors for check. In addition to that, the loop in change
detection support to remove `ExpressionChanged...` errors for these cases
can potentially cause infinite loops in rare cases when these views are
refreshed at a time when parents have only been traversed and
`markForCheck` is called. Prior to the loop, if this `markForCheck` was
combined with an actual change to binding values, this would cause
`ExpressionChangedAfterItWasCheckedError`.

For regular cases where we are refreshing a view, update a binding, and call
`markForCheck`, this is either `ExpressionChanged...` in views with
default change detection or no error at all (but without any state
update) in views that are `OnPush` since the `Dirty` flag is cleared at
the end of `refreshView` and `checkNoChanges` uses the same `Dirty` bit for
traversal (angular#45612).

The only case where this might cause different and unexpected behavior
is if change detection starts in the middle of a view tree via
`ChangeDetectorRef.detectChanges` _and_ a state update happens where the
state is read in a view above the root of the traversal along with `markForCheck`.
This might be the case for updating host bindings during change
detection. That said, we have never really intented to support updating
state during change detection, especially in a location above the
current spot in the current view tree. This violates unidirectional data
flow, a principle we have always intented to enforce outside of using
signals for state.
atscott added a commit to atscott/angular that referenced this issue Feb 8, 2024
…eing refreshed

This commit updates the internal `markViewDirty` logic to avoid marking
views dirty up to the root when we encounter a view that is already
being refreshed. This is wasteful because we clear the dirty bit at the
end of refreshing a view anyways so it really has no affect on that
view.

This change should reduce the number of tree traversals for replay
observables and async pipe. For these cases, when we execute change
detection on the view for the first time, the async pipe subscribes to
the observable and its value emits synchronously. `markForCheck` is then
called inside `AsyncPipe`, which traverses all the way up to the root
view. This happens for _every_ `AsyncPipe` binding that emits
synchronously on subscription.

Sometimes we can reach a view and refresh it when the parents have been
skipped. For example, this happens with views that have updated signals.
Ancestors are only traversed and not refreshed. Marking dirty all the
way up to the root in this case can result in those ancestor views being
marked with the dirty flag and it not being cleared.

This only became problematic when we started to support backwards
referenced transplanted views and changed signal change detection to
not mark ancestors for check. In addition to that, the loop in change
detection support to remove `ExpressionChanged...` errors for these cases
can potentially cause infinite loops in rare cases when these views are
refreshed at a time when parents have only been traversed and
`markForCheck` is called. Prior to the loop, if this `markForCheck` was
combined with an actual change to binding values, this would cause
`ExpressionChangedAfterItWasCheckedError`.

For regular cases where we are refreshing a view, update a binding, and call
`markForCheck`, this is either `ExpressionChanged...` in views with
default change detection or no error at all (but without any state
update) in views that are `OnPush` since the `Dirty` flag is cleared at
the end of `refreshView` and `checkNoChanges` uses the same `Dirty` bit for
traversal (angular#45612).

The only case where this might cause different and unexpected behavior
is if change detection starts in the middle of a view tree via
`ChangeDetectorRef.detectChanges` _and_ a state update happens where the
state is read in a view above the root of the traversal along with `markForCheck`.
This might be the case for updating host bindings during change
detection. That said, we have never really intended to support updating
state during change detection, especially in a location above the
current spot in the current view tree. This violates unidirectional data
flow, a principle we have always intended to enforce outside of using
signals for state.
@atscott atscott added P4 A relatively minor issue that is not relevant to core functions P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent and removed P1 Impacts a large percentage of users; if a workaround exists it is partial or overly painful P4 A relatively minor issue that is not relevant to core functions labels May 3, 2024
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 3, 2024
…debug check

This commit adds a feature that might be useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.
atscott added a commit to atscott/angular that referenced this issue May 6, 2024
This commit adds a feature that is useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.

Because this is an experimental, debug-only feature, it is okay to merge
during the RC period.
atscott added a commit to atscott/angular that referenced this issue May 6, 2024
This commit adds a feature that is useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.

Because this is an experimental, debug-only feature, it is okay to merge
during the RC period.
atscott added a commit to atscott/angular that referenced this issue May 7, 2024
This commit adds a feature that is useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.

Because this is an experimental, debug-only feature, it is okay to merge
during the RC period.
atscott added a commit to atscott/angular that referenced this issue May 7, 2024
This commit adds a feature that is useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address angular#45612.

Because this is an experimental, debug-only feature, it is okay to merge
during the RC period.
AndrewKushnir pushed a commit that referenced this issue May 7, 2024
…eck (#55663)

This commit adds a feature that is useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address #45612.

Because this is an experimental, debug-only feature, it is okay to merge
during the RC period.

PR Close #55663
AndrewKushnir pushed a commit that referenced this issue May 7, 2024
…eck (#55663)

This commit adds a feature that is useful for determining if an
application is zoneless-ready. The way this works is generally only
useful right now when zoneless is enabled. Some version of this may be useful in
the future as a general configuration option to change detection to make
`checkNoChanges` pass always exhaustive as an opt-in to address #45612.

Because this is an experimental, debug-only feature, it is okay to merge
during the RC period.

PR Close #55663
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 breaking changes core: change detection P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
Projects
None yet
Development

No branches or pull requests

6 participants