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?
answer
- writes after the view was checked
- children changing what parents rendered
- non-deterministic template expressions
- dev-mode comparison is lenient for objects
basics
~20 sNG0100 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.
solid answer
~50 sThe recurring causes are: a component setting a template-bound field in `ngAfterViewInit` or `ngAfterViewChecked`, after its own view was checked; a child component writing, directly or through a shared service, to state an ancestor has already rendered; a loading flag toggled synchronously during the check; and a method, getter or pipe in a template that returns a different value on every call, such as `Math.random()` or a formatted `Date.now()`. The verification pass does not use plain `===` for its verdict. When `Object.is` says the values differ, it asks a lenient dev-mode comparison: two array-like iterables count as equal if their elements are equal, and two non-iterable objects are always treated as equal. So a getter returning a new filtered array with the same items passes, while a method returning `Math.random()` fails. The lenience only covers the check; the real pass still sees a new reference every time.
code
ts · 23 linesimport {Component, ChangeDetectionStrategy} from '@angular/core';
@Component({
selector: 'app-order-summary',
changeDetection: ChangeDetectionStrategy.Eager,
template: `
<p>Open orders: {{ openOrders.length }}</p>
<p>Lucky number: {{ lucky() }}</p>
`,
})
export class OrderSummary {
orders = [{id: 1, open: true}, {id: 2, open: false}];
// New array each call, same elements: passes the dev check (still wasteful).
get openOrders() {
return this.orders.filter((o) => o.open);
}
// New primitive each call: NG0100 in development mode.
lucky() {
return Math.random();
}
}go deeper
Recall the classic trigger: setting a displayed field in ngAfterViewInit, and that random or time-based values in templates also trip it.
Explain each pattern as a write after the view was checked, and how the dev check's lenient equality treats arrays, objects and primitives.
Recognise that OnPush and signals change which patterns throw, and treat silent cases as bugs to find with exhaustive checking.
Encourage template purity and one-directional data flow as team conventions, so NG0100 becomes rare rather than a recurring debugging chore.
## The one rule behind every cause Angular checks views top-down, and in development mode it follows each pass with a **verification pass** that re-evaluates every binding it just checked. NG0100 is thrown when the verification pass gets a different value. Every cause is therefore some code that changes a bound value **after** the view displaying it was checked, or an expression that simply cannot return the same value twice. ## Pattern 1: writes in after-view hooks `ngAfterViewInit` and `ngAfterViewChecked` run after the component's view and its children have been checked. Setting a template-bound field there changes a binding that has already been rendered: ```ts ngAfterViewInit() { this.chartReady = true; // bound as {{ chartReady }}: NG0100 in an Eager view } ``` The NG0100 reference recommends setting initial values in the constructor or `ngOnInit` instead, or moving the work to a hook that runs before the view is checked. ## Pattern 2: children writing parent state A child component that writes to state its parent binds, directly or through a shared service, is writing *backwards*: the parent's bindings were evaluated before the child was checked. The Angular queries guide warns against writing state into parent or ancestor components for exactly this reason. ## Pattern 3: non-deterministic expressions A template expression is evaluated twice in development mode, once per pass. If it returns something different each time, the check fails even though no one "changed" anything: - a method that returns `Math.random()` (templates cannot reach the global `Math` directly, so it is always wrapped in a component member); - a getter that formats `Date.now()` to milliseconds; - a method with a side effect, such as incrementing a counter it also returns. ## Pattern 4: loading flags and synchronous work during a check A service call that sets `loading = true` and resolves synchronously while the tree is being checked, or an `effect()` used to copy one piece of state into another, can flip a bound value mid-pass. The signals guide warns that using effects to propagate state can lead to NG0100, infinite updates or needless cycles, and recommends `computed()` instead. ## Why fresh arrays do not throw The verification pass compares values in two steps: 1. If `Object.is(previous, current)` is true, nothing changed. 2. Otherwise it applies a lenient **dev-mode equality** before throwing: | Previous and current values | Treated as equal by the check? | |---|---| | Two array-like iterables with equal elements | Yes, compared element by element | | Two non-iterable objects (including `Date` instances) | Yes, always | | Two different primitives (numbers, strings, booleans) | No, NG0100 | That is why `get openOrders() { return this.orders.filter(o => o.open); }` does not trigger NG0100 even though it returns a new array on every call, while `{{ lucky() }}` with `lucky()` returning `Math.random()` does. The lenience exists to avoid false alarms from code that rebuilds equivalent objects; it is not an endorsement. The first pass still sees a new reference every time and pushes it into child inputs, which is a performance cost (see the template-cost topics) even when no error is thrown. ## A quick triage table | Symptom | Most likely pattern | |---|---| | Error appears only on first render | A write in `ngAfterViewInit` or a child's init hook | | Error appears on every change detection | A non-deterministic method or getter in the template | | Error names a parent while the code change was in a child | A child writing upwards through a shared service or an output handled synchronously | | Error disappears when you add a `console.log` or a breakpoint | Timing-dependent async work toggling a flag mid-check | ## Why a pattern may not throw in your app Not every backwards write produces an error: - The default verification pass skips **`OnPush`** views that are not dirty, and components are `OnPush` by default since v22. The bug then shows up only as stale UI. - Writes to **signals** read by a template mark that view for refresh, and Angular re-runs the affected views in the same tick, so the new value renders instead of throwing. A pattern that is silent today can still be a bug. `provideCheckNoChangesConfig({exhaustive: true})` makes the development check cover `OnPush` views as well.
- In Angular, why can a template-bound method with a side effect cause NG0100 even with no lifecycle hooks involved?Development mode evaluates every binding twice, once in the real pass and once in the verification pass. A method that increments a counter and returns it, or reads the clock, returns a different primitive the second time, so the check sees a changed value. Template expressions should be pure: derive values in `computed()` signals or fields updated by events, not in methods with side effects.
saying these in an interview costs you the question
- Returning a new array from a getter always throws NG0100.
- NG0100 only happens inside ngAfterViewInit.
- The dev check compares bindings with plain === and nothing else.
- If the app does not throw NG0100, its data flow must be correct.
- Using an effect() to copy state between signals avoids NG0100 safely.