20 May 2024
9 min

Expression changed error – Debugging algorithm

Debugging techniques

In the previous section, we identified several design patterns that commonly lead to
“Expression Changed After It Has Been Checked” error.
In this section we’ll see how to use debugging techniques to identify the leading cause.
The general high-level algorithm to debug the error is the following:

  1. identify the component, the binding and the expression that yields different values
  2. single out a component’s property that changes the expression results
  3. discover how/why the value for this property changes between the regular detectChanges run and the checkNoChanges verification check

To illustrate how this algorithm can be applied in practice, I’ll use two examples.
The setup from the previous section, where a directive opens a modal dialog via a dedicated service,
is a good example. Another interesting setup involves an OnPush parent component for which Angular skips the checkNoChanges check.

So let’s dive in.

A mischievous directive

Inside the template of the ExpDescendant component we put a logic to display a modal dialog when a button is clicked.
A root component ExpRootComponent subscribes to notification from The DialogService and renders the modal into the DOM.
The notifications to show the modal are transmitted through the DialogDirective.

Here’s the setup again:

@Component({
  selector: 'exp-root',
  providers: [DialogService],
  template: `
    <div class="viewport">
      <exp-desc-cmp></exp-desc-cmp>
    </div>
  `,
  styles: [
    `
      .viewport {
        height: 100%;
        margin-top: calc(100% / 2);
        padding: 20px;
      }
      :host {
        height: 100vh;
        overflow: auto;
      }
      :host.modal {
        overflow: hidden;
        background: #2569af4d;
      }
    `,
  ],
})
export class ExpRootComponent {
  @HostBinding('class.modal') public modal = false;

  constructor(dialogService: DialogService) {
    dialogService.onDialogsChange((dialogs: any) => {
      this.modal = dialogs.length > 0;
    });
  }
}

export class DialogService {
  dialogs = [];
  notification = new Subject();

  open(options) {
    const dlg = { ...options };

    this.dialogs.push(dlg);
    this.notification.next(this.dialogs);

    return dlg;
  }

  close(dlg) {
    const i = this.dialogs.findIndex((d) => dlg === d);

    if (i === -1) return;

    this.dialogs.splice(i, 1);
    this.notification.next(this.dialogs);
  }

  onDialogsChange(fn) {
    this.notification.subscribe(fn);
  }
}

@Directive({
  selector: '[dialog]',
})
export class DialogDirective {
  constructor(private dialogService: DialogService) {}

  dialog = null;

  @Input('dialog') set open(open: boolean) {
    if (open) {
      this.dialog = this.dialogService.open({});
    } else {
      this.dialogService.close(this.dialog);
    }
  }
}

@Component({
  selector: 'exp-desc-cmp',
  template: `
    <div>
      <button (click)="show = !show">{{ show ? 'close' : 'open' }}</button>
      <div [dialog]="show"></div>
    </div>
  `,
})
export class ExpDescendant {
  show = false;
}

The problem with this setup is when we run it the “changed after check” error pops up.

Image alt

Let’s start by identifying the binding and the component that causes the problem.
One option is to click on the function that ran right before the classProp directive in the error stack trace:

Image alt

and it’ll show us the binding and the expression that causes the problem:

Image alt

Another option is to pause the code execution before Angular throws the error and inspect the callstack.
Put a breakpoint inside throwErrorIfNoChangesMode function:

Image alt

Once the execution is paused, click through the callstack to pinpoint the component’s definition and the property
used in the binding expression:

Image alt

By using any of those methods we discover that the property modal is what makes the expression yield different values.

Tracking property changes

Now we need to figure how/why the property changes between detectChanges and checkNoChanges runs.
One way is to re-define the modal property of the ExpRootComponent in runtime as a setter and
log the stacktrace from inside the setter.
But since we can make changes to the sources, we will use the
Proxy object
to intercept all writes to the modal property:

@Component({...})
export class ExpRootComponent {
  @HostBinding('class.modal') public modal = false;

  constructor(dialogService: DialogService) {
    const self = new Proxy(this, {
      set(target, prop, value) {
        if (prop === 'modal') {
          console.trace(`'modal' prop changed`, value);
        }

        target[prop] = value;
        return true;
      }
    });

    dialogService.onDialogsChange((dialogs: any) => {
      self.modal = dialogs.length > 0;
    });

    return self;
  }
}

It is also very convenient to mark the detectChanges and checkNoChanges stages because we’re particularly
interested in the modal property update between those two.

We can do that by adding logpoints to the
tick
method of the ApplicationRef:

Image alt

When we click on the button in the application, that’s what we see logged into the console:

Image alt

This output clearly shows us that there’s an update to the value of the modal property value between detectChanges
and checkNoChanges stages.

The stacktrace logged from inside the interceptor can reveal lots of interesting information.
For example, we could simply click on the anonymous function that runs before the setter to show us the line of code
that makes the update:

Image alt

If we continue going down the stack, we will construct the entire chain of function calls that contributed to the error.
For example, here we can see that directive gets an updated value for the dialog property as part of checking
the ExpDescendant component:

Image alt

From there, the method dialogService.open is called that triggers a notification to the onDialogsChange subscription.
All this happens inside the change detection loop which triggers “changed after check” error.

We discussed the proper fixes for this particular application setup
in the previous section.

A curious case of the OnPush component

Let’s take a look at another very interesting setup that I found on StackOverflow.
Assume we know nothing about the code that looks like this:

@Component({
  selector: 'parent',
  template: `
    <div>Data is loaded: {{ dataSize > 0 }}</div>
    <button (click)="click()">Load data</button>
    <child [data]="data" (stats)="handleStatsChange($event)"></child>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ParentComponent {
  data = [];
  dataSize: number;

  click() {
    this.data = ['Data1', 'Data2'];
  }

  handleStatsChange($event) {
    this.dataSize = $event;
  }
}

@Component({
  selector: 'child',
  template: ` <div *ngFor="let item of data">{{ item }}</div> `,
})
export class ChildComponent {
  @Input() data;
  @Output('stats') statsEmitter = new EventEmitter();

  ngOnChanges(changes): void {
    let dataSize = changes['data'].currentValue.length;
    this.statsEmitter.emit(dataSize);
  }
}

By quickly scanning the code we can conclude that the author wants to load some data and display the status on the screen.
When we run this example, we get no error in the console.
But the status still shows false even after the data is loaded and items are rendered on the screen:

Image alt

That’s some interesting inconsistency. The question on StackOverflow stated that if they removed the OnPush on parent,
they would get the “changed after check” error. Once we make that change and run the code again,
indeed the error pops up in the console:

Image alt

We might assume that the conditions that triggered the error manifested itself as an inconsistency between
the application state and the status rendered on the screen. The error itself just didn’t appear in the console for
some reason until we removed OnPush from the parent component. Let’s try to figure out why it happens.

By clicking on the ParentComponent_Template line in the stacktrace, we can discover the expression that yields different values.
This reveals the expression ctx.dataSize > 0:

Image alt

The property that we need to track changes for is dataSize:

@Component({
  selector: 'parent',
  template: `
    <div>Data is loaded: {{dataSize > 0}}</div>
    <button (click)="click()">Load data</button>
    <child [data]="data" (stats)="handleStatsChange($event)"></child>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParentComponent {
  dataSize: number;
  ...
}

Let’s see how the value for this property changes between the detectChanges and checkNoChanges.

Tracking property changes

The dataSize property is updated inside the handleStatsChange method of the ParentComponent.
This method runs when the child component emits stats event:

@Component({
  selector: 'parent',
  template: `
    <div>Data is loaded: {{dataSize > 0}}</div>
    <button (click)="click()">Load data</button>
    <child [data]="data" (stats)="handleStatsChange($event)"></child>
  `,
})
export class ParentComponent {
  dataSize: number;

  handleStatsChange($event) {
    this.dataSize = $event;
  }
}

@Component({...})
export class ChildComponent {
  @Input() data;
  @Output('stats') statsEmitter = new EventEmitter();

  ngOnChanges(changes) {
    let dataSize = changes['data'].currentValue.length;
    this.statsEmitter.emit(dataSize);
  }
}

Since the stats event is emitted synchronously from inside ngOnChanges which runs as part of change detection,
it’s expected that we’ll get the error.

An interesting question is why there’s no error if the parent component defined as OnPush.

Let’s add a logpoint to the child component’s template to see how many times Angular
runs the update logic for the textInterpolate binding:

Image alt

We also have logpoints inside the tick method to mark a change detection stage.
When we run the application and check the console, we’ll discover that Angular runs the template function
just once for detectChanges, not twice:

Image alt

That means that checkNoChanges stage is skipped. We can assume that this happens because since the component is OnPush,
it’s marked as dirty on click and checked during detectChanges phase. But part of the check logic
resets the component’s state
to pristine (non-dirty) after it’s been checked:

export function refreshView(tView, lView, templateFn, context) {
  ...
  if (!isInCheckNoChangesPass) {
    lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
  }
}

this leads to skipping the checkNoChanges for the ParentComponent.

We can see it here:

Image alt

and because of this reset Angular doesn’t check ParentComponent during the subsequent checkOnChanges phase:

Image alt

If we remove the OnPush declaration, the update logic runs twice as expected and the checkNoChanges phase produces the error:

Image alt

Note that the type of binding doens’t have any effect on the situation. If we used ngIf instead of the
text interpolation to render the status text:

@Component({
  selector: 'parent',
  template: `
		<span *ngIf="dataSize > 0">Data is loaded</span>v>
    <button (click)="click()">Load data</button>
    <child [data]="data" (stats)="handleStatsChange($event)"></child>
  `,
})

we would get the same error:

Image alt

This time however a different binding would throw the error.
As you can see in the stacktrace that would be property binding that updates a value for ngIf expression.

The fix

So the fact that the error is not there when OnPush is used on the parent component is misleading and removing OnPush isn’t a proper fix.
Regardless whether the error is thrown or not, the application state and UI remain inconsistent –
the status text shows false, but it should show true. This situation is exactly what the “changed after check”
error is supposed to detect and alert users about, but with an OnPush component this erroneous situation goes unnoticed.

So what would be the proper fix? We could use detectChanges inside the handler like this:

@Component({...})
export class ParentComponent {
  data = [];
  dataSize: number;

  constructor(private cdRef: ChangeDetectorRef) {}

  click() {
    this.data = ['Data1', 'Data2'];
  }

  handleStatsChange($event) {
    this.dataSize = $event;
    this.cdRef.detectChanges();
  }
}

which will result in the proper update of the status and the following log output:

Image alt

Before the child component is checked, the value for dataSize is 0 . Once Angular checks the child component, the value is updated through the event emission. Once we run detectChanges manually from inside the handler, Angular updates takes the value 2 for dataSize and correctly renders the status on the screen. This workaround works and fixes the state incconsistentency.

Another approach is to use asynchronous update. Here’s how we can use the resolved promise approach:

@Component({...})
export class ChildComponent {
  @Input() data;
  @Output('stats') statsEmitter = new EventEmitter();

  ngOnChanges(changes): void {
    let dataSize = changes['data'].currentValue.length;
    Promise.resolve().then(() => {
      this.statsEmitter.emit(dataSize);
    });
  }
}

which will result in the proper update of the status and the following log output:

Image alt

Here you can see two change detection cycles running one after the other.
The second one is scheduled when we use Promise.resolve.
Instead of the resolved promise approach, we could achieve a delayed update through the async event like this:

@Component({...})
export class ChildComponent {
  @Input() data;
  @Output('stats') statsEmitter = new EventEmitter(true);

  ngOnChanges(changes): void {
    let dataSize = changes['data'].currentValue.length;
    this.statsEmitter.emit(dataSize);
  }
}

This will use setTimeout instead of the resolved promise.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.