Ch. 4 · Angular

ExpressionChangedAfterItHasBeenCheckedError in Angular (NG0100)

Fix Angular NG0100 ExpressionChangedAfterItHasBeenCheckedError: why the dev-mode check fires, and data-flow fixes that beat setTimeout.

~8 min readintermediateupdated Oct 4, 2026

The browser console (or a failing unit test) shows this:

ERROR RuntimeError: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'title': 'Loading'. Current value: 'Loaded'. Expression location: _Dashboard component. Find more at https://v20.angular.dev/errors/NG0100
    at throwErrorIfNoChangesMode (chunk-TBIPMNIT.js:9819:9)
    at bindingUpdated (chunk-TBIPMNIT.js:15049:9)
    at Module.ɵɵproperty (chunk-TBIPMNIT.js:21218:7)
    at Dashboard_Template (main.js:63:10)
Text

Angular rendered a binding, re-evaluated it as a sanity check, and got a different answer: the screen and your component state disagree. Angular 19 links to angular.dev rather than v20.angular.dev, and interpolations like {{ title }} omit the for 'title' part, but the code and the previous/current/location structure are the same in 19 and later.

Quick fix checklist

  • Read the three facts in the message: the old value, the new value and the component whose template holds the binding.
  • Search that component’s template for a binding that can produce the “Current value”, then find who writes it.
  • If a child writes the parent’s state in ngAfterViewInit, ngAfterContentInit or ngAfterViewChecked, move the write earlier or turn the state into a signal.
  • If a template getter returns Date.now(), Math.random() or a counter, compute the value once and store it.
  • If the value depends on a @ViewChild or @ContentChild query, use the signal queries viewChild() / contentChild() with computed().
  • Do not wrap the write in setTimeout or call detectChanges() as the fix.

Before you start

You need an Angular 19+ project in development mode (ng serve or ng test). Production builds skip the check, which is why the bug “disappears” after ng build: the inconsistency remains, Angular just stops reporting it.

Know the order of a pass: for each component, Angular updates its template bindings, runs its content hooks, refreshes its child components, and only then calls ngAfterViewInit/ngAfterViewChecked. For how signals and OnPush change that walk, read change detection with OnPush and signals.

Why it happens

Angular promises unidirectional data flow inside a pass: data moves from parent to child, and a finished view reflects the final state. To enforce this, development mode runs a second, read-only pass called checkNoChanges after every application tick (ComponentFixture.detectChanges() does the same in tests). It re-evaluates every binding and throws NG0100 when one differs from the value stored in the first pass.

The comparison is lenient for objects: primitives use Object.is, two different plain objects count as equal, and arrays are compared element by element. A getter returning a fresh { a: 1 } does not trigger NG0100 (it still wastes work and defeats OnPush inputs); a getter returning Date.now() always will.

Three patterns cause almost every NG0100:

  1. A child changes parent state after the parent was checked. The parent’s {{ title }} was rendered, then the child’s ngAfterViewInit (directly, through an injected parent, through a shared service or by emitting an output() the parent handles) writes title. The check pass sees the new value.
  2. A non-deterministic expression. {{ now }} where get now() { return Date.now(); }, a random ID generated in a getter, or a method with side effects.
  3. A query-dependent flag. hasFooter = !!this.footer set in ngAfterViewInit, where footer is a @ViewChild. Queries resolve after the template was already rendered, so any binding that reads the flag changes too late. See view query timing for when each query is available.

Step-by-step walkthrough

Step 1: Reproduce it in isolation

This is the smallest version of pattern 1. The parent renders title; the child changes it after the parent’s bindings were already evaluated.

import { AfterViewInit, Component, inject } from '@angular/core';

@Component({ selector: 'app-stats-panel', template: `<p>stats</p>` })
export class StatsPanel implements AfterViewInit {
  private dashboard = inject(Dashboard); // resolved at runtime, so the later declaration is fine

  ngAfterViewInit() {
    this.dashboard.title = 'Loaded';
  }
}

@Component({
  selector: 'app-dashboard',
  imports: [StatsPanel], // evaluated immediately, so StatsPanel must be declared above
  template: `<h1 [title]="title">{{ title }}</h1><app-stats-panel />`,
})
export class Dashboard {
  title = 'Loading';
}
TypeScript

Running this under ng serve in Angular 20 prints the error shown at the top of this page, and the heading on screen keeps saying Loading: the check pass throws instead of re-rendering, so the stale value stays until something else triggers change detection. The class name may appear with a leading underscore or a numeric suffix (_Dashboard, Dashboard2) because the dev build renames classes; strip that when you search your code.

Step 2: Read the message like a stack trace

  • Expression location names the component whose template holds the binding, not the component that wrote the value. Here it is Dashboard, but the bug lives in StatsPanel.
  • for ‘title’ (present on property bindings) is the bound property. For interpolations you only get the values, so search the template for something that could render Loaded.
  • Previous and current values tell you what changed. '0' to '3' usually means a count emitted by a child; a long number that differs in the last digits means a timestamp.

Then look at the stack trace. The useful frame is the compiled template function, here Dashboard_Template, which confirms which template holds the binding. The code that wrote the value is never in this trace, because the write happened earlier in the pass. To catch the writer, set a breakpoint on the assignment or temporarily turn the field into a setter that calls console.trace().

Step 3: Find who writes the value and when

Ask: “Is this value known before the parent’s template is checked?” If yes, compute it earlier (field initializer, constructor, the owner’s ngOnInit, or route data). If it is only known after a child renders, notify the parent in a way Angular can schedule, which today means a signal.

Step 4: Apply a data-flow fix

For pattern 1, make title a signal. A signal write marks the views that read it for refresh, and Angular re-runs them within the same tick before the check pass. Tested in Angular 19 and 20: this version renders Loaded with no error.

import { AfterViewInit, Component, inject, signal } from '@angular/core';

@Component({ selector: 'app-stats-panel', template: `<p>stats</p>` })
export class StatsPanel implements AfterViewInit {
  private dashboard = inject(Dashboard);

  ngAfterViewInit() {
    this.dashboard.title.set('Loaded');
  }
}

@Component({
  selector: 'app-dashboard',
  imports: [StatsPanel],
  template: `<h1 [title]="title()">{{ title() }}</h1><app-stats-panel />`,
})
export class Dashboard {
  readonly title = signal('Loading');
}
TypeScript

Better still, ask whether the child should write the parent at all; often the parent can own the data and pass it down as an input. For pattern 2, store the value once:

export class ReportHeader {
  readonly generatedAt = Date.now(); // evaluated once, stable for every check
}
TypeScript

For pattern 3, signal queries plus computed() make the dependency explicit:

import { Component, computed, contentChild, Directive } from '@angular/core';

@Directive({ selector: '[appCardFooter]' })
export class CardFooter {}

@Component({
  selector: 'app-card',
  template: `<section [class.has-footer]="hasFooter()"><ng-content /></section>`,
})
export class Card {
  private footer = contentChild(CardFooter);
  readonly hasFooter = computed(() => !!this.footer());
}
TypeScript

Writing signals in hooks is not free: if each refresh writes again, Angular gives up after a small number of reruns with NG0103 (infinite change detection). Write once, or derive with computed().

Worked scenario

A shell layout shows a page heading. Each routed page sets it.

// Broken
@Injectable({ providedIn: 'root' })
export class PageTitle {
  value = '';
}

@Component({
  selector: 'app-shell',
  imports: [RouterOutlet],
  template: `<header>{{ pageTitle.value }}</header><router-outlet />`,
})
export class Shell {
  protected pageTitle = inject(PageTitle);
}

@Component({ selector: 'app-orders', template: `<table>...</table>` })
export class Orders implements AfterViewInit {
  private pageTitle = inject(PageTitle);
  ngAfterViewInit() {
    this.pageTitle.value = 'Orders';
  }
}
TypeScript

On navigation, the console shows Previous value: ''. Current value: 'Orders'. Expression location: _Shell component. and the header stays empty. Diagnosis: Shell rendered the header first, then the routed page (a descendant of the outlet) changed a plain field the header reads.

Fix: the service exposes a signal, and the page sets it as soon as it is created, because the title does not depend on the view at all.

@Injectable({ providedIn: 'root' })
export class PageTitle {
  readonly value = signal('');
}

// Shell template: <header>{{ pageTitle.value() }}</header><router-outlet />

@Component({ selector: 'app-orders', template: `<table>...</table>` })
export class Orders {
  constructor() {
    inject(PageTitle).value.set('Orders');
  }
}
TypeScript

For static titles, the router’s title route property is even simpler, since nothing in the component tree has to write anything.

Common mistake

Wrapping the write in setTimeout (or Promise.resolve().then). The error goes away because the write moves to a later task, after the check pass. With Zone.js that task triggers another full change detection, so the user briefly sees the stale value and then a flicker. In a zoneless app a plain field written in a timeout may never render at all, because nothing tells Angular to refresh. You have traded a loud dev-mode error for a silent production bug.

Calling this.cdr.detectChanges() in ngAfterViewInit. This re-renders synchronously so the check sees matching values. It only works on the right view (the parent, not the child that wrote), costs an extra render every pass, and leaves the backwards data flow in place. Keep it for imperative integrations such as a third-party widget measuring the DOM.

Ignoring it because production “works”. Production skips the check, but the screen still shows the stale value until something else triggers change detection.

Verify the behavior

ComponentFixture.detectChanges() runs the same check, so a focused test catches regressions:

import { TestBed } from '@angular/core/testing';

describe('Dashboard', () => {
  it('renders the loaded title without NG0100', () => {
    const fixture = TestBed.createComponent(Dashboard);
    expect(() => fixture.detectChanges()).not.toThrow();
    expect(fixture.nativeElement.querySelector('h1').textContent).toBe('Loaded');
  });
});
TypeScript

Before the fix, this test fails with the NG0100 message; after it, both assertions pass. In the running app, enable “Preserve log” in DevTools and confirm no NG0100 appears when you navigate to the affected screen. The normal check skips OnPush components that were not marked dirty, so an OnPush tree can hide the same bug: temporarily add provideCheckNoChangesConfig({ exhaustive: true }) (Angular 20; provideExperimentalCheckNoChangesForDebug in 19) to the app providers to check every view.

Interview exercise

“Why does ExpressionChangedAfterItHasBeenCheckedError only happen in development, and is wrapping the assignment in setTimeout an acceptable fix?”

Answer and reasoning

Change detection is a single top-down pass, and a finished view should not change during it. Development mode verifies that by running a second, side-effect-free pass and comparing every binding; production skips it for speed. So the error is a diagnostic: the bug is code (usually a child lifecycle hook) changing state the parent already rendered, or a non-deterministic expression.

setTimeout defers the write past the check: the diagnostic goes quiet, but you get a second render, a visible flicker, and in zoneless apps possibly no re-render at all. A good answer fixes the data flow instead: compute the value before the parent renders, let the parent own the state, or use a signal so Angular re-runs the affected view in the same tick. Mentioning signal queries with computed() for the @ViewChild flag case shows practical experience.

Continue learning

More in Angular

read ✓Angular · hard

Angular Resource API for Async Data

Load data reactively with the resource API, track loading and error state as signals, and cancel stale requests automatically.

~2 min readread →
read ✓Angular · hard

Angular Signal Forms Overview

Model forms as signals, bind fields in the template, and reason about validation and derived state in a signal-driven form.

~2 min readread →
read ✓Angular · hard

Angular Zoneless Change Detection

Run Angular without zone.js, drive updates with signals, and understand which async sources still schedule change detection.

~2 min readread →
esc