Angular 2 change detection: what the hell is “… has changed after it was checked. Previous value: … Current value: …” exception?

Note: the source code used in this post is based on Angular 2 RC.4. Of course, the exception occurs in the previous versions as well. Nevertheless, the examples are using new features which might not be available in the previous (beta) versions.

During my work with Angular 2 I stumbled upon this exception couple of times. Being faced with this exception is not nice. On the other hand, it tells us that there is something wrong with the general workflow in the application. There is some kind of “data inconsistency”.

The change detection algorithm complains about it by throwing the exception “… has changed after it was checked. Previous value: … Current value: …”.

In big applications composed of many components it can be tricky to fix it. One possible solution is to change the detection strategy of the particular component but — mostly — this is just a way to workaround the actual issue. The following example is demonstrating this exception, how to figure out the cause and — finally — how to fix it.

Application workflow

In this application (https://github.com/jepetko/angular2-fruits-app) we have a budget and want to buy some fruits. Let’s add some apples and pears. Add as many of them until the budget is consumed (< 0). Now, the difference value is red because the total price > budget.

Now, go back to the budget input field, delete the value and press Tab key. When you look at the console you can see that the mentioned exception has been thrown:
Error: Expression has changed after it was checked. Previous value: ‘false’. Current value: ‘true’ at ExpressionChangedAfterItHasBeenCheckedException.BaseException [as constructor]

Let’s analyze the behavior of the component: there is a form (created by FormBuilder) containing a budget field and one field per fruit. Nothing special going on here… The input fields are bounded to the model and revalidated each time the value gets changed. The validator CustomValidators.doesBudgetCoverTheFruits has full access to the form model and the form as a whole. There is also kind of validation message telling the user that the budget needs to be increased.

How to figure out what’s going on

Now, the question is which expression is actually causing the exception. We can assume that the cause is a piece of code which gets executed every time when the value of budget gets changed. In Angular 2 components, quite every single line of code gets executed when the change detection mechanism tries to re-render the template. Luckily, this application is small so that we can identify every single expression:

  • the validation message is shown when the expression _hasBudgetCoverageError() is true
  • {{model.pricePerFruit}} shows the price per fruit
  • the summary is composed of expressions {{model.budget}}, {{model.getTotalPrice()}} and {{model.getDiff()}}

Let’s exclude the particular expressions from the template (Ceteris paribus). First, we remove the block <div class=”error” …></div> and repeat the user input as described above.

When we look at the console log we can see that the exception does not occur anymore. What does it mean? You guess it right:

the user input was manipulated *AFTER* the expressions depending on the user input were processed.

The budget field has a blur event handler which sets the budget default value to 100. Does it mean that we are not allowed to use the blur event handler? No. We just cause an expected data change in the method _hasBudgetCoverageError().

Now, we create a matrix where we check the application behavior consistency in terms of the change detection. Note that the expressions are listed in the same order as they are placed in the template because this plays a large role in the change detection mechanism of Angular 2.

#id  _hasBudgetCoverageError()  budget  getTotalPrice()  getDiff()
------------------------------------------------------------------
1 false 100 50 50
2    true                       100     200              -100
3    false                      (empty)  50               -50
4    false                      (empty)   0                 0

We know that the error message should appear (_hasBudgetCoverageError()) when the difference (getDiff()) between budget and getTotalPrice() is negative.

In the case #1 the message is not displayed because getDiff() is +50.

In the case #2 the message is displayed because getDiff() is -100.

In the case #3 the message does not appear even if the getDiff() returns -50? It’s obvious that this is the cause of the exception!

How to fix it

We now know that the implementation of the method doesBudgetCoverTheFruits is error-prone. When the budget is empty then parseInt returns NaN. NaN is not smaller than model.getTotalPrice() which means that undefined is returned (=the form is valid). In _hasBudgetCoverageError() undefined evaluates to false:

static doesBudgetCoverTheFruits(getModelFn: () => Model): {[key: string]: boolean} {
return (group: ControlGroup) => {
let model = getModelFn();
let budget: number = parseInt(model.budget);
if (budget < model.getTotalPrice()) {
return {doesBudgetCoverTheFruits: true};
}
};
}

We actually don’t want to perform any value comparisons if the budget is NaN. We want to say “hey, the budget value is invalid and therefore does not cover the fruits in your shopping bag”. So, let’s modify it:

static doesBudgetCoverTheFruits(getModelFn: () => Model): {[key: string]: boolean} {
return (group: ControlGroup) => {
let err = {doesBudgetCoverTheFruits: true};
let budget: number = parseInt(model.budget);
if (isNaN(budget)) {
return err;
}
let model = getModelFn();
return (budget < model.getTotalPrice()) ? err : undefined;
};
}

After this modification the behavior matrix was changed as follows and the exception is fixed as well.

#id  _hasBudgetCoverageError()  budget  getTotalPrice()  getDiff()
------------------------------------------------------------------
1 false 100 50 50
2    true                       100     200              -100
3    true (<---)                (empty)  50               -50
4    false                      (empty)   0                 0

Writing unit tests

The exception is gone but I wonder whether it would be avoidable in the future work. Might unit tests be the suitable tool for that? I set up a unit test and, well, it’s possible.

Look at the spec:

(Source: Angular 2 Fruits App)

Let’s decompose the spec into the particular steps:

  • first, we need to enable the auto detection. The auto detection is actually what happens in the browser. It does not need to be kicked off by calling detectChanges().
  • second, we need to patch the execution of the change detection specific tasks in the ngZone. By default, the task tick(…) is executed unguarded which would mean that we cannot catch the exception. Instead of using run(() => …) we have to use runGuarded(() => …).
  • third, we specify the test case. We subscribe on the zone’s onError handler, exhaust the budget (due to adding 100 apples), clear the budget input and leave it.
  • finally, the expectation checks whether the errHandler has been called.

Now, the question is whether it makes sense to write tests for such a scenario. In fact, it doesn’t. Rather than test for this particular exception we could perform a kind of monkey test.

Monkey tests for the user input

We could have some “explorative” tests by providing multiple user input values during the auto detection:

(Source: Angular 2 Fruits App)

The key idea is to repeat the user input with different values. The spec will fail if one of the provided input values causes a data inconsistency in terms of the change detection.