What does Angular's ExpressionChangedAfterItHasBeenCheckedError (NG0100) mean, and why do you only ever see it in development mode?
answer
- a second look at every binding
- previous value versus current value
- one pass should be enough
- production skips the extra pass
basics
~20 sNG0100 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 sAfter 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
Recall that NG0100 means a displayed value changed after Angular checked it, and that the check only runs in development builds.
Explain the verification pass, what the previous and current values in the message are, and why the stale value persists in production.
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.
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.