skip to content

Change Detection

How Angular decides which views to re-check: zone.js or zoneless scheduling, OnPush and Eager strategies, and ChangeDetectorRef control. Most 'the app feels slow' answers land here.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

27

What does Angular's ExpressionChangedAfterItHasBeenCheckedError (NG0100) mean, and why do you only ever see it in development mode?

level: juniorimportance: must knowfreq 72%

answer

  1. a second look at every binding
  2. previous value versus current value
  3. one pass should be enough
  4. production skips the extra pass

basics

~20 s

NG0100 means a template binding produced a different value in Angular's dev-mode verification pass than in the change detection pass just before it. The extra pass runs only in development builds, so production shows the stale value silently instead of throwing.

solid answer

~50 s

After every change detection run, a development build of Angular runs a second **verification pass** over the views it just checked. It re-evaluates each binding and compares the result with the value it stored during the real pass. If they differ, something changed state *after* that part of the tree was already checked, so the DOM no longer matches the model, and Angular throws `ExpressionChangedAfterItHasBeenCheckedError` with the previous value, the current value and the component where the expression lives. The pass doubles the cost of change detection, so production builds leave it out entirely. That does not make the bug go away: in production the view simply keeps the stale value until some later check happens to refresh it. The error is a warning that data is flowing backwards, typically from a lifecycle hook such as `ngAfterViewInit` or from a child writing state its parent has already rendered.

go deeper

for a junior

Recall that NG0100 means a displayed value changed after Angular checked it, and that the check only runs in development builds.

for a middle

Explain the verification pass, what the previous and current values in the message are, and why the stale value persists in production.

for a senior

Treat NG0100 as a data-flow bug, not noise: know which patterns cause it, why OnPush can hide it, and why signals change the outcome.

for a principal

Decide how strictly the team treats NG0100, for example failing tests on it and enabling exhaustive checks in development, given that production users only see silent staleness.

## What change detection promises Angular's **change detection** walks the component tree from the root down. For each view it evaluates the template's bindings, such as `{{ title }}` or `[disabled]="busy"`, compares each result with the value from the last run, and updates the DOM where they differ. The design assumes **one pass is enough**: once a parent's bindings have been evaluated, nothing later in the same pass should change them. If something does, the DOM now shows a value that no longer matches the component state. ## The verification pass In **development mode**, after `ApplicationRef.tick()` has synchronized the views, Angular runs a second pass in a special **check-no-changes mode**: 1. It walks the views again without touching the DOM. 2. It re-evaluates each binding and compares the result with the value stored in the first pass. 3. If a value differs, it throws **NG0100**. The message names what it found: ```text ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'Loading'. Current value: 'Report ready'. Expression location: Dashboard component ``` The **previous value** is what was rendered; the **current value** is what the state says now; the **expression location** tells you which component's template to inspect. ## Why only in development | | Development build | Production build | |---|---|---| | Verification pass | Runs after every change detection | Not included in the bundle | | Cost | Roughly a second traversal per check | None | | When state changes after being checked | Throws NG0100 | Stale value stays on screen until a later check | The production build is not "fixing" anything by omitting the pass; it is simply not looking. A user sees an outdated label, a spinner that does not disappear or a button in the wrong state, and there is no error to explain it. ## What it usually points at The NG0100 reference page lists the recurring causes: - a **lifecycle hook that runs after the view is checked**, such as `ngAfterViewInit` or `ngAfterViewChecked`, setting a value the template binds; - a **child component changing state its parent has already rendered**, for example through a shared service; - a **method or getter** called from the template that returns a different value each time; - **loading flags** toggled synchronously while the tree is being checked. All of them are the same shape: data flowing *backwards* against the top-down order in which Angular checks the tree. ## Reading the message in practice The fastest diagnosis starts from the three facts the error gives you: 1. **Expression location** names the component whose template holds the binding. That is the view that was checked too early relative to the change, not necessarily the code that made the change. 2. **Previous and current values** usually identify the binding by content: a loading label, a count, a CSS class string. 3. **The stack trace**, read with the CLI's source maps, leads to the template expression; from there, search for every place that writes the underlying state and ask which of them runs after that view was checked. In most cases the writer is a lifecycle hook that runs late (`ngAfterViewInit`, `ngAfterViewChecked`), a child component, or a method in the template that is not pure. ## What the error is not - It is **not** a bug in Angular or something to silence. It marks a real inconsistency between state and DOM. - It is **not** fixed by disabling development mode; that removes the messenger, not the problem. - It is **not** guaranteed to fire for every backwards write. The default verification pass skips `OnPush` views that were not marked dirty, and components are `OnPush` by default since v22, so some cases show up only as stale UI. Writes to signals are handled differently: Angular re-runs the affected views in the same tick instead of throwing. The right response is to find which code wrote the value late and move that write, or the state itself, so the value is settled before the view that shows it is checked.

  • In Angular, how do you find the code that triggers an NG0100 error?
    Read the message first: the expression location names the component and the previous and current values usually identify the binding. Then walk up the stack trace with source maps to the template expression, and search for code that writes that value after the view was checked, most often in `ngAfterViewInit`, `ngAfterViewChecked`, or a child component writing to a shared service.
  • If Angular production builds never throw NG0100, why fix it at all?
    Because the inconsistency is still there. In production the DOM keeps the value from the first pass, so users see a stale label or state until another check refreshes that view, which may be never for an OnPush component. The error is the only place the bug is visible, and fixing the data flow removes it in both modes.

It is like a proofreader who re-reads a printed page against the manuscript. If the author edited the manuscript after the page went to print, the proofreader flags it; skip the proofreader and the page ships with the old text.

saying these in an interview costs you the question

  • NG0100 is an Angular bug you can safely ignore.
  • Production builds fix the problem, which is why the error disappears.
  • The error means change detection ran twice by mistake.
  • Turning on production mode is an acceptable fix.
  • Every late state change is guaranteed to raise NG0100 in dev mode.
open as a page

In Angular, what is the difference between ChangeDetectorRef.markForCheck() and ChangeDetectorRef.detectChanges()?

level: juniorimportance: must knowfreq 72%

basics

~10 s

markForCheck() only marks the view and its ancestors dirty and notifies the scheduler, so the next change-detection pass refreshes them; detectChanges() synchronously checks this view and its descendants right now, without touching ancestors.

open as a page

In Angular 22, what is the difference between ChangeDetectionStrategy.OnPush and ChangeDetectionStrategy.Eager, and which does a component get by default?

level: juniorimportance: must knowfreq 82%

basics

~20 s

An Eager component is refreshed every time a change detection pass reaches it; an OnPush component is refreshed only when its view has been marked dirty. Since Angular 22, a component that omits changeDetection is OnPush.

open as a page

In a zone-based Angular app, how does zone.js tell Angular to run change detection after a setTimeout callback or a click handler?

level: juniorimportance: must knowfreq 68%

basics

~10 s

zone.js patches async browser APIs such as timers, promises and event listeners, so NgZone sees each callback run in the Angular zone; when its microtask queue drains, NgZone emits onMicrotaskEmpty and Angular calls ApplicationRef.tick().

open as a page

What does it mean that an Angular v21+ application is zoneless by default, and what does dropping zone.js gain you?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Since v21, Angular schedules change detection from its own notifications (template-read signal writes, markForCheck, bound listeners, setInput) instead of zone.js patching browser APIs. Dropping zone.js means fewer needless checks, a smaller bundle, faster startup and readable stack traces.

open as a page

In Angular, what marks an OnPush component's view dirty so that the next change detection pass actually re-checks it?

level: middleimportance: must knowfreq 72%

basics

~20 s

A template-bound input receiving a new reference, an Angular-bound event firing in the component or a descendant, a markForCheck() call, or a change to a signal its template reads. Mutations and plain field writes mark nothing.

open as a page

In a zoneless Angular application, which notifications schedule change detection, and why does a field assigned inside setTimeout never render?

level: middleimportance: must knowfreq 58%

basics

~20 s

A zoneless Angular app checks only after a notification: a template-read signal is set, markForCheck() runs, a bound listener fires, setInput() is called, or a dirty view is attached. A setTimeout callback assigning a plain field sends none, so nothing refreshes.

open as a page

In a zone-based Angular app, how do NgZone.runOutsideAngular() and NgZone.run() work together, and what breaks if you forget to re-enter?

level: middleimportance: must knowfreq 48%

basics

~20 s

Code started in runOutsideAngular(), and every callback it schedules, runs outside the Angular zone and triggers no checks. run() re-enters for the update that matters; forget it and a plain-field update, even in a parent's output handler, never renders.

open as a page

An Angular child component sets a page title in a shared service during ngAfterViewInit, and the parent's header binding throws NG0100; why, and what is the real fix?

level: seniorimportance: must knowfreq 55%

basics

~20 s

The parent's header binding was evaluated before the child's ngAfterViewInit ran, so the child changed a value already rendered. Hold the title in a signal the header reads, so Angular re-renders it in the same tick, or set it earlier; setTimeout only hides it.

open as a page

An Angular OnPush order-table component shows no new row after the parent calls this.orders.push(newOrder); why, and how would you fix it?

level: seniorimportance: must knowfreq 66%

basics

~20 s

push() mutates the array in place, so the [orders] binding sees the same reference, the OnPush table is never marked dirty, and its @for never re-runs. Replace the array instead, ideally held in a signal and updated with a new copy.

open as a page

In a zone-based Angular app, why does a setInterval that changes nothing on screen still make Angular run change detection on every tick?

level: juniorimportance: should knowfreq 40%

basics

~20 s

zone.js patches setInterval, so each callback that runs inside the Angular zone looks like work that might have changed state, and Angular checks the whole view tree afterwards. Starting the timer with NgZone.runOutsideAngular() stops those needless checks.

open as a page

Which code patterns typically trigger Angular's NG0100 error, and why does a getter returning a fresh array not trigger it while a method returning Math.random() does?

level: middleimportance: should knowfreq 55%

basics

~20 s

NG0100 comes from state changed after its view was checked: writes in ngAfterViewInit or ngAfterViewChecked, children changing parent-bound state, and non-deterministic template expressions. The dev check compares arrays element by element and treats plain objects as equal, so a rebuilt array passes while a new random number throws.

open as a page

In Angular, what does calling ApplicationRef.tick() do compared with ChangeDetectorRef.detectChanges(), and why is calling it from application code rarely right?

level: middleimportance: should knowfreq 35%

basics

~20 s

ApplicationRef.tick() synchronously runs change detection for every view attached to the application, plus root effects and render hooks, with a dev-mode verification pass; detectChanges() checks one subtree. Schedulers already call tick(), so manual calls usually duplicate work or fail as recursive ticks.

open as a page

In Angular's NgZone, how do onMicrotaskEmpty and onStable differ, and why is onStable not a signal that all pending async work has finished?

level: middleimportance: should knowfreq 42%

basics

~20 s

onMicrotaskEmpty fires, possibly several times per turn, whenever the Angular zone's microtask queue drains, and drives ApplicationRef.tick(); onStable fires once after the last onMicrotaskEmpty, outside the Angular zone, and says nothing about timers or requests still pending.

open as a page

In Angular, how would you use ChangeDetectorRef.detach() and detectChanges() to refresh a live stock-ticker widget once per second, and what does detaching cost you?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Call detach() so ticks skip the widget, then call detectChanges() from a one-second interval to render the latest quotes; while detached, nothing else refreshes it, not input changes, markForCheck() or signal reads, until reattach() or the next manual check.

open as a page

An Angular codebase calls cdr.detectChanges() after nearly every subscribe callback to make OnPush views update; what problems does that hide, and what would you replace it with?

level: seniorimportance: should knowfreq 45%

basics

~20 s

It hides state Angular cannot track: each call is an unbatched synchronous subtree check that leaves ancestors stale and papers over leaking subscriptions. Replace it with signals, toSignal() or the async pipe, and markForCheck() only for unavoidable callbacks.

open as a page

After an Angular 22 upgrade, your team wants to delete the ChangeDetectionStrategy.Eager lines ng update added; what must you audit in each component first?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Audit every way template data changes without marking the view: fields set in subscriptions, timers or promises, mutated input objects, inputs written through @ViewChild, and mutable service state. Convert those to signals or immutable updates, starting with leaf components.

open as a page

After upgrading an Angular app to v22, a new countdown component that decrements a plain field in setInterval never updates on screen; which two version changes do you check?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Zoneless became the default in v21, so without provideZoneChangeDetection() a timer triggers no tick; and since v22 components are OnPush by default, so even a zone tick skips a new component whose plain field changed. A signal fixes both.

open as a page

When moving an Angular app to zoneless, why must code that waits on NgZone.onStable or onMicrotaskEmpty change, and what replaces it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

In a zoneless Angular app, NgZone.onStable, onMicrotaskEmpty and onUnstable never emit and NgZone.isStable is always true. Code waiting for a check to finish should use afterNextRender (one render) or afterEveryRender (each render), or a direct DOM API.

open as a page

In a zoneless Angular app, a third-party map widget's move callback assigns this.center, but the displayed coordinates never update; why, and how do you fix it?

level: seniorimportance: should knowfreq 46%

basics

~20 s

The widget calls back through its own event API, not an Angular-bound listener, and no zone.js observes it, so assigning a plain field notifies nothing. Store the value in a signal the template reads and set() it in the callback.

open as a page

A zone-based Angular dashboard embeds a chart library that tracks mousemove and scroll for tooltips, and the whole page lags; how do you confirm zone pollution and fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Angular DevTools' profiler shows a run of change detection cycles whose source is mousemove, scroll or requestAnimationFrame while the pointer moves. Initialise the chart inside NgZone.runOutsideAngular() and re-enter with run(), or set a signal, only for events that change Angular-rendered state.

open as a page

How would you plan moving a large zone.js-based Angular application to zoneless change detection without a risky big-bang cutover?

level: principalimportance: should knowfreq 30%

basics

~20 s

Make components notify Angular first (signals, AsyncPipe, markForCheck, OnPush-compatible), remove NgZone observable dependencies, switch tests to zoneless, verify with exhaustive dev checks, then drop provideZoneChangeDetection and zone.js from polyfills, one app or shell at a time.

open as a page

In Angular's provideZoneChangeDetection(), what do the eventCoalescing and runCoalescing options change, and what is the trade-off of enabling them?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

eventCoalescing merges the ticks from several DOM event handlers into one change detection; runCoalescing does the same for repeated NgZone.run() calls. Both defer that tick to the next animation frame or timeout, so views update slightly later.

open as a page

What does Angular's provideCheckNoChangesConfig({exhaustive: true, interval}) change about the NG0100 check, and why enable it once components default to OnPush?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

The default NG0100 check skips OnPush views that are not dirty, so with OnPush as the v22 default many late writes go unreported. exhaustive: true verifies every attached view as if Eager; interval also re-runs the check periodically.

open as a page

In Angular, when a signal read in an OnPush component's template changes, which views does the next pass re-check, compared with markForCheck()?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Only the view whose template read the signal is refreshed; its ancestors are flagged just so the pass can reach it, so OnPush ancestors are not re-checked because of it. markForCheck() marks the view and every ancestor dirty, so the whole path is refreshed.

open as a page

In a zoneless Angular SSR app, why would you use PendingTasks, and how do its add() and run() methods hold off serialization?

level: seniorimportance: nice to knowfreq 27%

basics

~20 s

Without zone.js, Angular cannot see arbitrary async work, so SSR might serialize before data arrives. PendingTasks registers work that must finish first: add() returns a cleanup function to call when done, and run() wraps an async function and removes the task when it settles.

open as a page

Does moving an Angular application to zoneless change detection eliminate zone pollution, and which needless checks can still remain?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

Mostly yes: without zone.js, timers, animation frames and third-party listeners trigger no checks, and runOutsideAngular() becomes a plain call. What remains are high-frequency template or host listeners, which still notify on each event, and code that writes signals on every event.

open as a page