What does Angular's provideCheckNoChangesConfig({exhaustive: true, interval}) change about the NG0100 check, and why enable it once components default to OnPush?
answer
- the default check skips some views
- dirty flag cleared before verifying
- treat every view as Eager
- a periodic check for zoneless
basics
~20 sThe 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.
solid answer
~50 sThe verification pass behind NG0100 only re-checks views the normal pass would refresh. An `OnPush` view's dirty flag is cleared when it is refreshed, so by the time the verification pass runs it is skipped; the Angular source notes that this check never worked for `OnPush` components. Since v22 components are `OnPush` by default, so a growing share of backwards writes shows up only as stale UI. `provideCheckNoChangesConfig({exhaustive: true})` makes the verification treat every view attached to `ApplicationRef`, and its descendants, as `Eager`, skipping only subtrees detached with `detach()`. Adding `interval: <ms>` also runs that check periodically, which in a zoneless app throws NG0100 for bindings changed without any notification. The provider does nothing in production builds. It is developer preview, was renamed from `provideExperimentalCheckNoChangesForDebug` in v20, and makes development change detection slower, so teams often enable it in dev and end-to-end environments.
go deeper
Know that Angular's NG0100 check can be made stricter, and that it only affects development builds.
Explain why the default check skips non-dirty OnPush views and what exhaustive and interval each add.
Enable exhaustive checking deliberately, plan for the backlog it reveals, and use the interval mode during zoneless migrations.
Decide where strict checking runs (local, CI, e2e) and weigh slower development builds against catching silent staleness before users do.
## The gap in the default check In development mode Angular follows every change detection with a **verification pass** that re-evaluates bindings and throws **NG0100** when one has changed. By default that pass only visits views that the normal pass would refresh: - **`Eager` views** (the old `Default` strategy) are always refreshed, so they are always verified. - **`OnPush` views** are refreshed only when marked dirty (by an input change, a bound event, `markForCheck()` or a signal the template reads). Refreshing clears the dirty flag, so when the verification pass arrives, the view looks clean and is **skipped**. The Angular source says it plainly: the check that runs after regular change detection does not work for `OnPush` components. For years that affected only teams that opted into `OnPush`. Since **v22**, components without a `changeDetection` setting are `OnPush` by default, so the gap now covers most new code. The result is a class of bugs with no alarm: a child writes to state its parent already rendered, the default check does not look at the parent, and the UI silently shows the old value. ## What the provider changes `provideCheckNoChangesConfig` takes one of two shapes: | Options | Effect | |---|---| | `{exhaustive: false}` | The default behaviour: only views the normal pass would refresh are verified | | `{exhaustive: true}` | Every view attached to `ApplicationRef`, and all descendants, is verified as if it were `Eager`; subtrees detached with `ChangeDetectorRef.detach()` are still skipped | | `{exhaustive: true, interval: 1000}` | As above, plus the check runs on its own every 1000 ms, not only after change detection | The **exhaustive** mode surfaces NG0100 errors that `OnPush` has been hiding. The **interval** mode exists mainly for **zoneless** applications: if some code changed a bound value without notifying Angular (a plain field set in a timer or a third-party callback), no change detection runs at all, so nothing would be verified. A periodic check finds those bindings and throws NG0100 for them. ## Enabling it ```ts import {ApplicationConfig, provideCheckNoChangesConfig} from '@angular/core'; export const appConfig: ApplicationConfig = { providers: [provideCheckNoChangesConfig({exhaustive: true, interval: 1000})], }; ``` Points to know before turning it on: 1. **Development only.** In a production build the function returns no providers, so leaving it in the config costs nothing at runtime. 2. **Slower development runs.** Verifying every view on every change detection is more work than the default; large apps may notice it. 3. **Expect a backlog.** Turning it on in an existing app often surfaces many errors at once, all real bugs that were hidden. 4. **Developer preview.** The API is marked developer preview since v20, when it replaced `provideExperimentalCheckNoChangesForDebug` and dropped that API's `useNgZoneOnStable` option. ## What it typically finds When teams switch exhaustive mode on, the reports cluster around a few patterns: - child components or directives writing to a parent's state through a shared service after the parent was checked; - `ngAfterViewInit` and `ngAfterViewChecked` hooks that set template-bound fields in `OnPush` components; - template methods whose result depends on time or randomness, inside `OnPush` components that the default check never re-verified; - in zoneless apps (with `interval`), plain fields assigned in timers, WebSocket handlers or third-party callbacks. Each report names the component and the previous and current values, exactly like any other NG0100, so the usual diagnosis applies. ## Where it fits A practical pattern is to enable exhaustive checking in local development and in end-to-end test environments, fix the backlog feature by feature, and keep it on so new backwards writes fail fast. It pairs naturally with a zoneless migration, where the interval check is one of the few tools that detects state changed without a notification. It does not replace fixing the data flow. When it reports NG0100, the fixes are the usual ones: move the write earlier, let data flow downward, or hold shared state in signals so Angular can re-run the affected views in the same tick.
- Why does Angular's default NG0100 check miss OnPush components?The verification pass re-uses the normal traversal rules, and an OnPush view is refreshed only when dirty. Refreshing clears that dirty flag, so when the verification pass runs the view is no longer dirty and is skipped. Angular kept this behaviour deliberately so existing hidden errors would not suddenly surface; exhaustive mode is the opt-in to check them.
- Does provideCheckNoChangesConfig affect an Angular production build?No. The provider only registers its configuration when Angular is in development mode; in a production build it returns an empty provider list, and the verification pass itself is not part of production change detection. It can stay in the application config without runtime cost.
saying these in an interview costs you the question
- The default NG0100 check already covers OnPush components fully.
- Exhaustive checking also runs in production to catch real users' bugs.
- exhaustive: true re-checks views detached with detach() as well.
- The interval option fixes bindings that changed without a notification.
- No NG0100 errors with OnPush proves the data flow is correct.