question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Clarify docs or improvement to avoid ExpressionChangedAfterItHasBeenCheckedError

See original GitHub issue

I’m submitting a…


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[X] Feature request
[X] 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

Current behavior

As evident from numerous stack overflow threads, blog articles and issues here, people stumble upon the ExpressionChangedAfterItHasBeenCheckedError time and again. In some cases, people just make mistakes. Other common behaviors include something like

  1. Child (or later sibling) gets its first onChanges and emits an output.
  2. Parent changes state

NOTE: This case also fails in these cases

{{child.childOutput | async}}
<child #child>

Expected behavior

I think everyone would benefit from a clear guidance on the recommended way of treating this. If needed, the documentation should then also be updated. Btw, right now I don’t find much at all on @Output.

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

So how should it work then? Some suggestions.

  1. Just use new EventEmitter(true) for your outputs where this could happen. This really needs to be made clear then in the documentation. And the bad thing with this approach is that it doesn’t work for val | async bindings - a case where this is a problem as well. And there is potentially a benefit with not using e.g. .debounceTime(0) globally on the Observable. Sure, in these cases the client could have its own observable with .debounceTime(0), but it quickly becomes messy with many cryptic solutions.
  2. Manually handle this in the parent component by calling cdRef.markForCheck() or cdRef.detectChanges(). IMO cumbersome. And…which one should I use and why? And is this even enough always. Doesn’t the async pipe already do this? Or should I run Zone.current.scheduleMicroTask('', () => cdRef.markForCheck())? Again, document the whats and whys?
  3. Improve Angular to automatically call e.g. cdRef.markForCheck() (or via microtask pattern above) when an event is handled through a template binding in the “Detect changes in children” phase. Isn’t this already done btw? Is doing it via a microtask key here?

Number 3. seems to be the easiest on clients and least error prone, that would be my choice. Of the cons I could imagine is “How do you detect circular changes?”. That’s a hard one. But, we’re not solving it by urging everyone do do new EventEmitter(true). That, to me, just looks like an anti-pattern to hide such errors behind asynchronous bars. My suggestion for that would be to do something similar to Angular 1, and keep a flag, essentially doing something like cdRef.markForCheckButDoneAsResultOfChildOutput() - and if appRef.ticks are run with that flag more than, say 10 times, yield an error. Alternatively check which bindings were marked and not let the same change detector ref be marked twice in the same macro task.

Environment


Angular version: 4.4.6

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:2
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

4reactions
staekecommented, Nov 17, 2017

@tytskyi - thanks for your reply

i think this only happens when you update child.childOutput in child’s lifecycle hook

right

I think documentation should be clarified in the way that the component should not update parent’s state during the component’s lifecycle hook synchronously. However i have no idea where such details can fit in documentation?

Documentation

First, I think this is a difficult and big enough knowledge domain to require its own page in the documentation. Something like “Data flows” or “Inputs/Outputs”. One paragraph there could be specifically about this error, which would be linked from the actual exception message in the console. Thus, users would find it. At least those who stumble upon it.

Secondly, users looking in documentation need to know how to solve their problems. Not primarily what not to do. In other words, what are some common scenarios where you’d face this problem, and how would you solve them. Let me give you an example:

Concrete example

In the HeaderComponent there’s a button that opens up a WelcomeGuideComponent. The WelcomeGuideComponent can also be opened by other means, e.g. an internal keyboard shortcut listener that it sets up. Or via query param changes. The HeaderComponent is not responsible for giving it its initial value. Whenever it’s open, the HeaderComponent button should have a special css class. Naturally I think most people would try something like

<!-- HeaderComponent -->
<button (click)="wg.open()" [class.open]="open">Open Guide</button>
<welcome-guide #wg (openChange)="open = $event"></welcome-guide>

…but if WelcomeGuide in its ngOnInit checks queryParams, and emits an initial openChange.emit(true), this breaks. I disagree that this would be very rare. On the contrary, my impression (and I think the results on stack overflow/google support this) is quite common. So, telling people that “the component should not update parent’s state during the component’s lifecycle hook synchronously” is, albeit correct, not very helpful. How should the user solve it then? It seems to me, the general consensus now (as recommended in other sources) is to use new EventEmitter(true) for openChange.

For discussion’s sake, let me broaden this a bit. Say we have multiple WelcomeGuide components but only one should be visible at a time. Thus we have a little WelcomeGuideSvc, which keeps track of the active (if any) and broadcasts events about the active one to all WelcomeGuide components. Every WelcomeGuide component is responsible to listen on these events and close if needed. Now, should I use the setTimeout(() => welcomeGuideSvc.setActive(this)) or just welcomeGuideSvc.setActive(this)? Where lies the responsibility? Maybe we say that as a rule you must not have any side effects outside of your component as a synchronous result of ngOnChanges. That sounds complicated, but you realize a setTimeout does the job. That’s fine, but let’s assume now there’s state to indicate that several WelcomeGuides should open initially. They all see that WelcomeGuideSvc.active === null and display. As our event is asynchronous we see un unfortunate flicker at first render since they appear in the DOM only to be closed (all but one) on next tick. So you’re damned if you do, and damned if you don’t.

Moving on to another point…let’s get back to the original case, what if you make the EventEmitter asynchronous. And then do (bad code)

// HeaderComponent.ts
set open(val) {
   this._open = val && noOtherHeaderItemsOpen; // <-- In example: noOtherHeaderItemsOpen === true
}
get open() {
  return this.openData.val;
}
// WelcomeGuide.ts
set open(val) {
   this._open = val && welcomeGuidesSupported; // <-- In example: welcomeGuidesSupported === false
   this.openChange.emit(this.open);
}
<button (click)="wg.open()" [class.open]="open">Open Guide</button>
<welcome-guide #wg [(open)]="open"></welcome-guide>

Now this may seem contrived, but I’ve seen similar cases in real life. This error is very hard to debug. The browser will likely just freeze. If we supported synchronous events, there would be a possibility to build a detector (Angular 1 style) and throw an error.

Then we get back to the various solutions I outlined in my initial post with some pros and cons. But met le reiterate.

  1. Tell people that “you must not have any side effects outside your component as a synchronous result of ngOnChanges”. Run setTimeout, new EventEmitter(true) etc. when appropriate + solves most problems - complex to understand for Angular users - not one easy single solution - there are scenarios that are not solved - can result in undetected asynchronous infinite loops
  2. Support this better in Angular + solves problem + very simple for Angular users + enables building guards for some asynchronous infinite loops

I miss valid cons with the second one.

1reaction
atscottcommented, May 26, 2021

This is a really great writeup! Thanks for the input @staeke. I’m going to close this and roll it into #33731.

Read more comments on GitHub >

github_iconTop Results From Across the Web

ExpressionChangedAfterItHasBe...
I had a similar issue. Looking at the lifecycle hooks documentation, I changed ngAfterViewInit to ngAfterContentInit and it worked.
Read more >
Everything you need to know about the ... - InDepth.Dev
Everything you need to know about the `ExpressionChangedAfterItHasBeenCheckedError` error. This article explains the underlying causes of the error and the ...
Read more >
Angular Debugging "Expression has changed": Explanation ...
Learn a complete explanation about ExpressionChangedAfterItHasBeenCheckedError: why it occurs, how to troubleshoot it and how to fix it.
Read more >
ExpressionChangedAfterItHasBe...
The ExpressionChangedAfterItHasBeenCheckedError error is thrown up when the binding expression changes after being checked by Angular during the ...
Read more >
Teradata/covalent - Gitter
ill fix it in the quickstart and docs with the proper way, without using Promise ... created a PR to update documentation on...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found