skip to content

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%

answer

  1. writes after the view was checked
  2. children changing what parents rendered
  3. non-deterministic template expressions
  4. dev-mode comparison is lenient for objects

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.

solid answer

~50 s

The 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 lines
ts
import {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

for a junior

Recall the classic trigger: setting a displayed field in ngAfterViewInit, and that random or time-based values in templates also trip it.

for a middle

Explain each pattern as a write after the view was checked, and how the dev check's lenient equality treats arrays, objects and primitives.

for a senior

Recognise that OnPush and signals change which patterns throw, and treat silent cases as bugs to find with exhaustive checking.

for a principal

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.