Deep dive into the infamous ExpressionChangedAfterItHasBeenCheckedError in Angular

maxkoretskyi

Max

Posted on January 31, 2023

Deep dive into the infamous ExpressionChangedAfterItHasBeenCheckedError in Angular

Overview

One of the most frequent Angular related topics on StackOverflow is about the infamous ExpressionChangedAfterItHasBeenCheckedError error. Usually these questions come up because developers do not really understand why the check that produces this error is required. Many even view it as a flaw in the framework design. However, for Angular it’s simply a way to enforce unidirectional data flow and ensure that application state and UI are in sync after a single pass of change detection cycle.

Unidirectional data flow entails that once Angular has processed bindings for the current component, you can no longer update the properties of that component that make up bindings expressions. Angular implements checkNoChanges method that runs after a regular change detection cycle and re-evaluates binding expressions. If during this check Angular detects that an expression produces different value compared to the value it produced during the preceding detectChanges run, it will throw ExpressionChangedAfterItHasBeenCheckedError.

A binding defines the property name to update and the expression that produces a value for the property. Bindings are encoded as rendering engine (Ivy) instructions which Angular’s compiler puts into the component’s
template function. When Angular checks a component view during change detection, it runs over all bindings by executing corresponding instructions. For each binding, it evaluates expressions and compares the results to the previous value produced by the expression. That’s where the name dirty checking comes from.

If those values differ, Angular updates the property defined by the binding. That’s what happens in the regular change detection cycle triggered by the detectChanges method. However, when running in special checkNoChangesMode that is triggered by checkNoChanges method, when the difference is detected, instead of updating the binding the Expression Changed error is thrown.

The function that checks for the difference and throws the error is bindingUpdated. Here’s a bit simplified implementation of this function:

export function bindingUpdated(lView, bindingIndex, value) {
  const oldValue = lView[bindingIndex];

  // no update is needed
  if (Object.is(oldValue, value)) {
    return false;
  } else {
    // if we're in development mode and the values are not equal,
    // throw the error and return without updating the binding
    if (ngDevMode && isInCheckNoChangesMode()) {
      const oldValueToCompare = oldValue !== NO_CHANGE ? oldValue : undefined;
      if (!devModeEqual(oldValueToCompare, value)) {
        const details = getExpressionChangedErrorDetails(...);
        throwErrorIfNoChangesMode(oldValue === NO_CHANGE, details.oldValue, ...);
      }
      return false;
    }
    lView[bindingIndex] = value;
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can do a quick usage search to identify all Ivy instructions that use bindingUpdated and hence can throw the error:

Image description

Interestingly, there’s also a unit-test that validates the logic that produces the error. Here’s how it checks if the property instruction that updates the [id] property correctly throws the error:

class MyApp {
      unstableStringExpression: string = 'initial';

      ngAfterViewChecked() {
        this.unstableStringExpression = 'changed';
      }
}

it('should include field name in case of property binding', () => {
  const message = `Previous value for 'id': 'initial'. Current value: 'changed'`;
  expect(() => initWithTemplate('<div [id]="unstableStringExpression"></div>'))
      .toThrowError(new RegExp(message));
});
Enter fullscreen mode Exit fullscreen mode

Test specs use ngAfterViewChecked lifecycle hook to update the property as this hook is triggered after the bindings have been processed. In the it('should include field...) spec we saw above, the setup starts with the value "initial" for the unstableStringExpression component property. Then inside the ngAfterViewChecked hook, the value is set to "changed". The checkNoChanges verification cycle will throw the ExpressionChangedAfterItHasBeenCheckedError error because it detects the difference.

All this means that the root cause for the error is always the same - different value that comes to a binding during the regular detectChanges cycle and the validation detectNoChanges run. Accordingly, the primary fix is also always the same - ensuring the expression yields the same value during the regular detectChanges cycle and the verification checkNoChanges run.

This test suit basically defines all possible cases when bindings will throw the error:

initWithTemplate('<div [id]="unstableStringExpression"></div>')
initWithTemplate('<div id="Expressions: {{ a }}')
initWithTemplate('<div [attr.id]="unstableStringExpression"></div>')
initWithTemplate('<div [style.color]="unstableColorExpression"></div>')
initWithTemplate('<div [class.someClass]="unstableBooleanExpression"></div>')
initWithTemplate('<div i18n>Expression: {{ unstableStringExpression }}</div>')
initWithTemplate('<div i18n-title title="Expression: {{ unstableStringExpression }}"></div>')
initWithHostBindings({'[id]': 'unstableStringExpression'})
initWithHostBindings({'[style.color]': 'unstableColorExpression'})
initWithHostBindings({'[class.someClass]': 'unstableBooleanExpression'})
Enter fullscreen mode Exit fullscreen mode

The particular fix for the error depends on the leading cause
which is something that leads to the binding getting different values. To find the leading cause we need to know what binding gets different values and where the difference in values comes from. Usually finding this leading cause isn't straightforward and requires the use of lots of interesting debugging techniques to pinpoint the culprit and we’ll explore in-depth how to do that in the next section finding the primary cause.

Let’s now take a look at a very simple illustration of this verification mechanism works.

The error mechanism in details

To see how the error is detected and thrown we’ll implement a component that renders a random number obtained through Math.random():

@Component({
  selector: 'p-cmp',
  template: `
    <h3>
      <button (click)="noop()">Generate number</button>
    </h3>
    <div [textContent]="number"></div>
  `
})
export class P {
  noop() {}
  get number() {
    return Math.random();
  }
}
Enter fullscreen mode Exit fullscreen mode

The number expression used in the [textContent] binding gets different values during a regular and verification change detection cycles. Because of that fact, when we run the above application we can expect the error:

Image description

The leading cause here is that Math.random() produces different results each time it's called. So the value that textContent binding gets during the detectChanges will never match the value it receives for the checkNoChanges run.

The error we got gives us the following information:

ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
Previous value for 'textContent': '0.9449286101652989'. Current value: '0.1686897170657191'.
Find more at https://angular.io/errors/NG0100

Just as we assumed, it tells us that the values produced by the expression number for the textContent binding were different during a regular change detection cycle and the verification run. This textContent binding is processed by the property instruction and we can see it in the callstack. The error itself comes from the bindingUpdated function we discussed above:

Image description

We can even see it using Chrome dev tools:

Image description

That’s actually might be quite surprising to you because usually that error comes up in a lot more sophisticated scenarios. But can you think of a very trivial fix for this case? 🤓

This particular case actually matches this spec from above mentioned unit-test suite:

initWithTemplate('<div [id]="unstableStringExpression"></div>')
Enter fullscreen mode Exit fullscreen mode

Of course, real world examples are much more intricate and complex. They usually involve a component hierarchy and an interaction mechanism between ancestors and their descendent components. This interaction is often indirect and happens through shared services, event emitters or observables. It takes good understand and lots of practice to quickly discover the leading cause.

To illustrate the error mechanism that involves a hierarchy, let’s look at the very basic setup with a parent and a child component.

Updating ancestors

We’re defining a simple hierarchy of two components. A parent component declares the text property that is used in an interpolation binding. A child component injects the parent component through DI into the constructor and updates its text property in the ngAfterViewChecked hook. Just like Angular’s test suite, we use this hook because it’s triggered after the bindings have been processed.

Here’s the code snippet for the setup:

@Component({
  selector: 'q-cmp',
  template: `
    <h3>Q1 text: {{text}}</h3>
    <q1-cmp></q1-cmp>
  `
})
export class Q {
  text = 'initial';
}

@Component({
  selector: 'q1-cmp',
  template: ``
})
export class Q1 {
  constructor(private q: Q) {}

  ngAfterViewChecked() {
    this.q.text = 'updated';
  }
}
Enter fullscreen mode Exit fullscreen mode

And as expected we get the error:

Image description

This time our demo setup corresponds to this spec from the unit tests suite:

initWithTemplate('<div id="Expressions: {{ a }}')
Enter fullscreen mode Exit fullscreen mode

The relevant instruction is interpolation and we can see it in the callstack:

Image description

Interestingly, sometimes we could even get the error if we update the property in the ngOnInit hook:

@Component({
  selector: 'q-cmp',
  template: `
    <q2-cmp></q2-cmp>
    <h3>Q2 text: {{text}}</h3>
  `
})
export class Q {
  text = 'initial';
}

@Component({
  selector: 'q2-cmp',
  template: ``
})
export class Q2 {
  constructor(private q: Q) {}

  ngOnInit() {
    this.q.text = 'updated';
  }
}
Enter fullscreen mode Exit fullscreen mode

Here’s the error again:

Image description

What might be very surprising, but actually makes sense, is that the error won’t happen if swap elements in a template of the Q component from this:

<h3>Q2 text: {{text}}</h3>
<q2-cmp></q2-cmp>
Enter fullscreen mode Exit fullscreen mode

to this:

<q2-cmp></q2-cmp>
<h3>Q2 text: {{text}}</h3>
Enter fullscreen mode Exit fullscreen mode

Basically we’re placing <q2-cmp> before the interpolation update. Run the example, no error pops up.

The magic is in a template function that’s generated by the compiler. Here’s the order of instructions in the tempate function for the original order:

Image description

Compare it to the swapped elements setup:

Image description

You can see that when <q2-cmp> precedes the interpolation update Q2 text: {{text}}, the hook ngOnInit is executed from the template function Q_Template and that happens before Angular updates the DOM. That’s why there’s no error. However, if <q2-cmp> follows the interpolation update, the ngOnInit hook is executed through executeInitAndCheckHooks after the text property has been processed by the template function and hence the binding throws the error.

💖 💪 🙅 🚩
maxkoretskyi
Max

Posted on January 31, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Angular Form Array
angular Angular Form Array

November 29, 2024

Can a Solo Developer Build a SaaS App?
undefined Can a Solo Developer Build a SaaS App?

November 29, 2024

Angular's New Feature: Signals
javascript Angular's New Feature: Signals

November 29, 2024