Search code examples
angularangular-changedetection

ExpressionChangedAfterItHasBeenCheckedError on basic button disable


I made a very basic form. During submission the button is disabled and once the work is done it should be enabled.

However this doesn't work, instead I get the infamous ExpressionChangedAfterItHasBeenCheckedError.

I read Max Koretskyi articles about CD and this exception but can't understand why this happens in this case? It is a simple reaction to an event. Could someone explain in detail what exactly causes this and how could I fix it without setTimeout or Promise?

Here is the sample code:

https://stackblitz.com/edit/angular-change-detection-err?file=src/app/app.component.ts


Solution

  • I fork your Stack-Blitz and make the error disappear by triggering detectChanges() on your finalize.

    Stack-Blitz with detectChanges

    I will try to explain why this is happening. The error seems to be trigger on finalize because it runs after the async pipe, when the Observable completed.

    In other words angular trigger the detection check (as always does after a async operation if your component is not OnPush) but after the detection function is finished the values in the properties checked are not the same and that's the reason that you get that error. If you trigger manual the change detection on finilize Angular will check for changes again and everything will be OK.

    (Edited on 2020-09-14)

    Another solution is to add a delay(0) to your pipeline before finalize or tap functions. more details here: Angular-Debugging.

    And that's the sample code of our example: Stack-Blitz with delay

    (Edited on 2019-09-16)

    Debugged the code for a while and now it is clear what happens. Event handler update() runs on submit, sets submissionInProgress to true, and sets the submissionResult$ observable . Change detection kicks in after the event. It evalutes other bindings if there are (in the order of your template) then the submissionInProgress binding, records its value as true. The next binding is the async pipe for submissionInProgress$. The async pipe notices that its input is not null anymore and subscribes to it. However of(true) results a sync observable hence during subscription the observable evalutes. OnNext emits, asyn pipe's _updateLatestValue method sets its _latestValue field, the observable completes and the finalize method runs. Finalize method updates submissionInProgress field to false. Notice that angular already passed the submissionInProgress binding and recorded its value as true, but we just updated the field by finalize. CD records that the asyn pipe binding has a value true now. After these the 2nd round of CD kicks in where it checks for no changes. It evaluates the submissionInProgress binding and sees that the value was true but now it false so it throws the exception. If we add a delay or setTimeout, subscription will happen but no business logic will run, the code will remain in the task queue?. Stack empties, task queue is processed, the modell will be updated with the emitted value and the finalize's side effect submissionInProgress = false. After these will kick in the CD and that's fine now.

    Useful points for debugging:

    • your event handler's enter and exit
    • core.js: view.detectChanges();, view.checkNoChanges();, function bindingUpdated(lView, bindingIndex, value)
    • binding points in your html like *ngIf="async..."
    • common.js: class AsyncPipe { functions