Debugging ExpressionChangedAfterItHasBeenChecked (NG0100) in Angular [2026]

Link copied
Debugging ExpressionChangedAfterItHasBeenChecked (NG0100) in Angular [2026]

Debugging ExpressionChangedAfterItHasBeenChecked (NG0100) in Angular [2026]

ExpressionChangedAfterItHasBeenCheckedError is the Angular error people most often "fix" without understanding: wrap the assignment in setTimeout, call detectChanges(), and move on. The error goes away, but the bug it was pointing at stays, usually as a flicker, a stale value, or a component that renders twice. The error itself is rarely the problem. It's a development-mode alarm telling you that data is flowing in a direction Angular didn't expect.

This is lesson 10.2 of the Angular Tutorial. Lesson 10.1 listed NG0100 in the error catalog; this lesson goes deep on it. You'll learn why the check exists, the five root causes behind nearly every occurrence, the fix for each, and how signals and zoneless change detection change how often you meet it.

Why the check exists #

Angular renders top-down. During a change detection pass it visits a component, evaluates each binding, updates the DOM, then moves to the children. The model assumes one direction of data flow per pass: by the time a view has been checked, nothing in that pass should change the values it displays.

In development mode, Angular verifies that assumption. After each pass it runs a second, read-only pass (checkNoChanges) that re-evaluates bindings and compares them with what it just rendered. If any value differs, the DOM is already showing something stale, so Angular throws NG0100 with the old and new values.

Mode What happens when a binding changes mid-pass
Development The verification pass notices and throws NG0100
Production No verification. The DOM keeps the old value until something triggers the next check
Production, with a later trigger The new value appears on the next pass, often seen as a flicker or delay

That's why "it only happens in dev" is not a reason to ignore it. Production users get the stale value, silently.

The verification pass also re-evaluates your template expressions, so a template method runs at least twice per check in development. That's expected; it's not a "double render".

Reading the message #

The error tells you which binding changed and its two values:

NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
Previous value: 'Loading'. Current value: 'Dashboard'.

Click through the stack trace to the first frame in your code. With source maps on, Angular points at the component; the "previous" and "current" values usually make the binding obvious. If they don't, search the template for every binding that could produce one of those values.

The five root causes #

1. Assigning state in ngAfterViewInit or ngAfterViewChecked #

By the time these hooks run, the component's view has already been checked. Changing a value its template reads now is the textbook case:

@Component({
  selector: 'app-panel',
  template: `<p>Width: {{ width }}px</p><div #box></div>`,
})
export class Panel implements AfterViewInit {
  box = viewChild.required<ElementRef<HTMLElement>>('box');
  width = 0;

  ngAfterViewInit() {
    this.width = this.box().nativeElement.offsetWidth;   // NG0100
  }
}

Fix: if the value doesn't depend on the DOM, set it earlier (field initializer, constructor, ngOnInit). If it does depend on measuring the DOM, make it a signal and set it in afterNextRender(). Signal writes there schedule a fresh render instead of breaking the current one:

width = signal(0);

constructor() {
  afterNextRender(() => {
    this.width.set(this.box().nativeElement.offsetWidth);
  });
}
<p>Width: {{ width() }}px</p>

2. A child changes data its parent already rendered #

The parent is checked before its children. If a child writes to shared state during its own initialization, the parent's already-rendered binding is now stale:

// Parent template: <h2>{{ layout.title }}</h2> <app-orders />
@Component({ selector: 'app-orders', template: `...` })
export class Orders implements OnInit {
  private layout = inject(LayoutService);
  ngOnInit() {
    this.layout.title = 'Orders';     // parent already showed the old title: NG0100
  }
}

Fix: make the shared state a signal, so the parent's binding is a reactive read:

@Injectable({ providedIn: 'root' })
export class LayoutService {
  readonly title = signal('Home');
}
<h2>{{ layout.title() }}</h2>

When a child writes to a signal that an already-checked view reads, Angular marks that view for refresh again rather than leaving it stale. Even better, set the value from where it belongs: route title or route data, which the parent can read without the child reaching back up.

3. A template expression that returns a new value each time #

Getters and methods that produce a fresh value on every call fail the verification pass, because "previous" and "current" differ even though nothing meaningful changed:

// ✗ different value on every evaluation
get timestamp() { return Date.now(); }
get id() { return Math.random().toString(36).slice(2); }

For object or array bindings, the same applies to identity: a getter that returns { ...this.config } passes a new object to a child input on every check.

Fix: compute the value once and store it, or use computed() so it's only recalculated when its sources change:

readonly id = crypto.randomUUID();                 // once per instance
readonly options = computed(() => ({ ...this.config(), compact: true }));

4. Template methods with side effects #

A method called from a template that also writes state changes other bindings in the middle of a pass:

// template: {{ label() }} ... {{ views }}
label() {
  this.views++;                // side effect during rendering
  return `Viewed ${this.views} times`;
}

With plain fields this produces NG0100. If views is a signal, Angular refuses the write outright with NG0600, which is the same bug caught earlier.

Fix: keep template expressions pure. Move the write to an event handler, a lifecycle hook, or the code that loads the data.

5. Effects or subscriptions that write state the view already read #

An effect that copies one value into another runs after the change, so the second value lands late. Angular's documentation specifically warns that using effects to propagate state can cause ExpressionChangedAfterItHasBeenChecked errors, infinite loops or extra change detection cycles. The same is true for an RxJS subscription that synchronously sets a plain field during change detection.

// ✗ state propagation through an effect
query = signal('');
page = signal(1);
constructor() {
  effect(() => { this.query(); this.page.set(1); });
}

// ✓ derive it
page = linkedSignal({ source: this.query, computation: () => 1 });

Fix: use computed() for derived values and linkedSignal() for derived values that the user can also override. Keep effects for side effects that leave Angular (storage, logging, third-party libraries).

Fixes that hide the problem #

"Fix" Why it seems to work What's wrong with it
setTimeout(() => this.x = y) The write happens after the pass, so the check passes Forces an extra pass, can flicker, and in zoneless apps a plain field write in a timeout doesn't refresh the view at all
cdr.detectChanges() in the hook Re-renders immediately so the check sees the new value Treats the symptom; repeated across components it becomes a cascade of synchronous renders
Promise.resolve().then(...) Same as setTimeout, one microtask later Same problems
Switching to Eager change detection More frequent checks eventually pick up the value Does more work everywhere and keeps the wrong data flow

Each of these can be the right tool in a narrow case, for example integrating a third-party widget that reports its size asynchronously. As a default reaction to NG0100, they make the code harder to reason about.

How signals and zoneless change the picture #

New Angular apps are zoneless by default since v21, and new components are OnPush by default in v22. That shifts which NG0100 causes you'll meet:

Situation Plain fields Signals
Value changed after its view was checked NG0100 in dev, stale in production The view is marked for refresh and updated
Write during template rendering or inside computed() NG0100 (if it changes a binding) NG0600 thrown immediately, at the write
Late value from setTimeout or a callback In a zoneless app, no refresh at all The signal write schedules a refresh
Derived value Assigned imperatively, easy to get out of order computed(), always consistent

Two practical consequences:

  • Signals turn most NG0100 causes into non-issues because a late signal write is a normal, scheduled update rather than a stale binding. Causes 1, 2 and 5 largely disappear when the state is a signal.
  • The check still exists. Components with plain fields, methods that return fresh values (cause 3), and libraries that mutate state during rendering still trigger it in development, zoneless or not.

For zoneless apps there's an extra debugging aid: provideCheckNoChangesConfig({ exhaustive: true, interval: 1000 }) periodically checks every view, including OnPush ones, and reports bindings that changed without notifying Angular. Use it temporarily in development when migrating a codebase.

// app.config.ts, development only
providers: [
  provideCheckNoChangesConfig({ exhaustive: true, interval: 1000 }),
],

A debugging routine #

  1. Read the previous and current values in the message and find the binding that produces them.
  2. Find the write. Search for every assignment to that field. Which one runs after the view was checked: a lifecycle hook, a child, a getter, a template method, an effect or subscription?
  3. Match it to a cause from the five above.
  4. Fix the data flow: set earlier, convert to a signal, derive with computed() or linkedSignal(), or move DOM-dependent writes to afterNextRender().
  5. Confirm that the error is gone without adding setTimeout or detectChanges().

Gotchas #

Symptom Cause Fix
NG0100 with a value from offsetWidth or getBoundingClientRect DOM measured in ngAfterViewInit, assigned to a plain field Signal set in afterNextRender()
NG0100 on a page title or breadcrumb in the shell A routed child writes shared state during init Signal in the service, or route title and data
Previous and current values look identical A getter returns a new object or array Store it, or use computed()
NG0100 only in tests fixture.detectChanges() runs the dev-mode check after you changed a field mid-test Set inputs and state before the first detectChanges(), or use signals
Error disappeared after adding setTimeout, but the UI flickers Two passes, first with the stale value Fix the source of the late write
NG0600 after converting to signals The write happens during rendering or inside computed() Move it out of the reactive context; derive instead of writing

What's next #

Lesson 10.3 tackles the opposite symptom: the data changed, but the component doesn't update. You'll debug OnPush and zoneless components step by step: whether the signal really changed, whether the template reads it, and whether something like untracked() or a plain field write is hiding the change.

Try it yourself #

Search your project for setTimeout( and detectChanges() inside lifecycle hooks. For each one, check whether it was added to silence NG0100, remove it in a branch, and see if the error returns. If it does, find which of the five causes applies and fix the data flow instead.

YouOur shell shows the page title from LayoutService.title, and each routed page sets it in ngOnInit. We get NG0100 on every navigation and someone wrapped it in setTimeout. What’s the proper fix?
Claude · used get_best_practicesThis is the child-writes-to-parent case: ShellComponent renders layout.title before the routed page’s ngOnInit changes it. The smallest correct fix: make title a signal() in LayoutService, call layout.title.set(...) from the pages, and read layout.title() in the shell template. A late signal write schedules a refresh of the shell instead of leaving a stale binding. For static titles you can go further and drop the page code entirely: put title: 'Orders' on each route and provide a custom TitleStrategy whose updateTitle() writes the resolved title into that same signal. Either way, remove the setTimeout: in your zoneless app it only works by accident, because something else happens to trigger the next render.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied Angular When Angular is launched ? February 8, 2024 · 1 min read When was Angular launched? # Angu…

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

1 comment

1 thought on “Debugging ExpressionChangedAfterItHasBeenChecked (NG0100) in Angular [2026]”

  1. Pingback: Why Isn't My Angular Component Updating? [2026]

Leave a Comment

Your email stays private. Required fields are marked *

1 thought on “Debugging ExpressionChangedAfterItHasBeenChecked (NG0100) in Angular [2026]”

  1. Pingback: Why Isn't My Angular Component Updating? [2026]

Leave a Comment

Your email stays private. Required fields are marked *