Deep dive into the infamous ExpressionChangedAfterItHasBeenCheckedError in Angular
Max
Posted on January 31, 2023
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;
}
}
We can do a quick usage search to identify all Ivy instructions that use bindingUpdated
and hence can throw the error:
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));
});
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'})
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();
}
}
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:
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:
We can even see it using Chrome dev tools:
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>')
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';
}
}
And as expected we get the error:
This time our demo setup corresponds to this spec from the unit tests suite:
initWithTemplate('<div id="Expressions: {{ a }}')
The relevant instruction is interpolation and we can see it in the callstack:
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';
}
}
Here’s the error again:
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>
to this:
<q2-cmp></q2-cmp>
<h3>Q2 text: {{text}}</h3>
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:
Compare it to the swapped elements setup:
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.
Posted on January 31, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.