Skip to content

Fix: Angular ExpressionChangedAfterItHasBeenCheckedError

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix ExpressionChangedAfterItHasBeenCheckedError in Angular caused by change detection timing issues, lifecycle hooks, async pipes, and parent-child data flow.

When This Hits You

I lost the better part of a week to this error on a production analytics dashboard, and the worst part was that the page worked fine, until I switched the build back to development mode for a small feature, and the console lit up with red. You run your Angular application in development mode and see this:

ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError:
Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.

Or variations like:

ExpressionChangedAfterItHasBeenCheckedError:
Expression has changed after it was checked. Previous value for 'ngIf: false'. Current value: 'ngIf: true'.
ExpressionChangedAfterItHasBeenCheckedError:
Expression has changed after it was checked. Previous value: 'null'. Current value: '[object Object]'.
ExpressionChangedAfterItHasBeenCheckedError:
Expression has changed after it was checked. Previous value for 'class.active: false'. Current value: 'class.active: true'.

The error only appears in development mode. In production, Angular skips the second check cycle that detects this mismatch. That does not mean the problem is harmless. It means your data is changing at the wrong time, which can lead to visual glitches, stale values, and hard-to-trace bugs.

Quick Reference Before You Dive In

If you arrived here from Google with a red Angular console, the five facts that resolve roughly 90 percent of cases:

  1. This error only fires in development mode. Angular runs change detection twice in dev to catch this mismatch; production runs it once. The error never appears in production but the underlying timing bug is still there. The official NG0100 error reference is the canonical resource.
  2. Most cases are getters or pipes that return a new reference on every call. A getter like get items() { return this.list.filter(...) } returns a fresh array each call; Angular sees a different value on the second pass and throws. Cache the computed value in a field or use a BehaviorSubject.
  3. The second most common cause is a child component modifying parent state during init. Children should emit events, not mutate parent data directly. Unidirectional data flow is the durable answer.
  4. setTimeout and ChangeDetectorRef.detectChanges() suppress the symptom; they rarely fix the cause. A setTimeout defers the change to the next cycle, which works but hides the real timing bug. Reach for them only after the diagnostic timeline below identifies an unavoidable post-check write.
  5. ChangeDetectionStrategy.OnPush prevents the error AND improves performance. It also forces you to think about input references, which surfaces the timing bug at design time. I default every new component to OnPush and require a documented reason to switch to Default.

The rest of this article walks through each of those in detail, plus the failure modes most other guides skip.

What Angular Is Actually Doing

Angular’s change detection runs in a unidirectional data flow. During each change detection cycle, Angular checks every bound expression in your templates to see if values have changed. In development mode, Angular runs this check twice. If a value changes between the first and second check, Angular throws ExpressionChangedAfterItHasBeenCheckedError.

Here is the sequence:

  1. Angular triggers change detection (e.g., after an event, HTTP response, or timer).
  2. Angular checks all template bindings and updates the DOM.
  3. In dev mode only, Angular runs a second check to verify stability.
  4. If any binding returns a different value during this second check, the error is thrown.

The root cause is always the same: something modifies a bound value after Angular has already checked it during the current cycle. This typically happens when:

  • A child component modifies parent data during initialization or lifecycle hooks.
  • A lifecycle hook like ngAfterViewInit changes a bound property after Angular has finished checking the view.
  • Getter methods or pipes return new object references on every call.
  • Async operations complete and update data at the wrong time in the change detection cycle.
  • A shared service emits a new value during change detection.

Understanding the change detection cycle is key. Angular checks the parent component first, then its children. If a child changes something that affects the parent’s template bindings during its own initialization, the parent’s bindings become stale, and the second verification check catches this inconsistency.

The first time I hit this, I sprinkled setTimeout(() => ..., 0) across the codebase and called it fixed. It “worked” for two months, then we shipped a refactor that exposed the original timing bug as data corruption in shared state. Suppressing the error with setTimeout or detectChanges() without understanding why a value changed is the kind of fix that costs you twice. Always answer why the value is changing before you choose a workaround.

Diagnostic Timeline

The first instinct is to sprinkle ChangeDetectorRef.detectChanges() until the error stops. That hides the problem instead of fixing it. Walk this timeline first; most cases resolve within five minutes.

Minute 0: Read the error verbatim. The full message names the changed value:

ExpressionChangedAfterItHasBeenCheckedError:
Expression has changed after it was checked.
Previous value: 'false'. Current value: 'true'.

Those two values are your only clue. A 'false' -> 'true' swap points to a boolean flag flipping during a lifecycle hook. A null -> [object Object] swap points to async data assignment. A 'ngIf: false' -> 'ngIf: true' is a structural directive driven by a property that just mutated.

Minute 1: Confirm it only fires in dev mode. Angular runs change detection twice in development to catch this mismatch. The error never appears in production, but the underlying bug is still there. If the error magically disappeared after a ng build --configuration=production, you suppressed the alarm, not the bug.

Minute 2: Identify the offending property via the stack trace. Open DevTools, expand the error stack, and find the first frame inside your own components. The template binding above that frame is the one whose value changed. In Angular 9+, the stack trace includes the template line number.

Minute 3: Locate the mutation site. Search for any code that writes to that property:

grep -rn "this\.\(isLoaded\|hasContent\|isFormValid\)\s*=" src/

Then ask which lifecycle hook the mutation runs in. The hook tells you the cause:

  • constructor or top-level field initializer → too early, move to ngOnInit
  • ngOnInit with a synchronous subject (BehaviorSubject, of()) → emits during parent’s check
  • ngAfterContentInit → usually safe, but check if the change propagates upward
  • ngAfterViewInit → too late, parent already checked; needs markForCheck or setTimeout
  • A getter or pipe (look for get someProp() returning a new array/object every call) → recompute on every check

Minute 5: Rule out side-effecting getters and pipes. This is the most-missed cause. Add a console.count('getter') inside any suspicious getter. If it fires more than twice per cycle, you have a referential-equality problem. Every call returns a new object reference, so Angular’s second pass sees a different value:

get visibleItems() {
  console.count('visibleItems'); // Should fire at most twice
  return this.items.filter(i => i.visible); // New array each call
}

Minute 7: Check parent-child data flow. If the change happens inside a child component but the binding is on the parent, the child violated unidirectional data flow. Open the child’s ngOnInit/ngAfterViewInit and look for any mutation of a @Input() value or a shared service property that the parent reads.

Minute 10: Only now reach for setTimeout or detectChanges(). If the timeline above identified an unavoidable post-check write (e.g., reading @ViewChild properties), Fix 2 and Fix 3 are the durable answers, but you should reach them with a hypothesis, not as a first move.

When to Use Which Fix

The next eight sections cover the fixes in detail. The table below maps your symptom to the recommended fix.

Your symptomRecommended fixWhy
State set in the constructor changes during parent’s checkFix 1: move to ngOnInitConstructor runs too early
@ViewChild value read in ngAfterViewInit triggers binding changeFix 2: cdr.detectChanges() after the readView init is after parent’s check, needs re-check
Third-party callback runs outside Angular’s zoneFix 3: setTimeout or Promise.resolve().then(...)Defers to next change-detection cycle
async pipe with synchronous BehaviorSubject / of() emissionFix 4: observeOn(asyncScheduler)Defers emission to next microtask
Child component mutates parent state during initFix 5: convert to @Output() event flowRestores unidirectional flow
Default change detection runs too aggressivelyFix 6: ChangeDetectionStrategy.OnPushReduces check surface area, prevents whole class of timing bugs
Read of @ViewChild form/component property in ngAfterViewInitFix 7: subscribe to statusChanges instead of synchronous readReactive pattern avoids the verification mismatch
Getter or pipe returns new array/object reference on every callFix 8: BehaviorSubject + distinctUntilChanged, or cache in ngOnChangesStable reference for change detection

If multiple rows apply, pick the topmost one.

Fix 1: Move Logic to ngOnInit Instead of the Constructor

If you set or modify bound properties in the constructor, the timing can conflict with change detection. The constructor runs before Angular has initialized the component’s bindings.

Broken: setting state in the constructor:

@Component({
  selector: 'app-user-profile',
  template: `<div *ngIf="isLoaded">{{ userName }}</div>`
})
export class UserProfileComponent {
  isLoaded = false;
  userName = '';

  constructor(private userService: UserService) {
    this.userService.getUser().subscribe(user => {
      this.userName = user.name;
      this.isLoaded = true; // Changes value during parent's check cycle
    });
  }
}

Fixed: move to ngOnInit:

@Component({
  selector: 'app-user-profile',
  template: `<div *ngIf="isLoaded">{{ userName }}</div>`
})
export class UserProfileComponent implements OnInit {
  isLoaded = false;
  userName = '';

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.userService.getUser().subscribe(user => {
      this.userName = user.name;
      this.isLoaded = true;
    });
  }
}

ngOnInit runs after Angular has set up the component’s input bindings, making it the proper place for initialization logic. The constructor should only be used for dependency injection.

This pattern applies broadly: any data fetching, subscription setup, or state initialization belongs in ngOnInit, not in the constructor. If you are working with TypeScript components and Angular, this lifecycle distinction is critical.

Fix 2: Use ChangeDetectorRef.detectChanges()

When you genuinely need to update a bound value after Angular has checked it (for example, after a ViewChild becomes available), you can explicitly tell Angular to re-run change detection.

Broken: modifying state in ngAfterViewInit:

@Component({
  selector: 'app-tabs',
  template: `
    <div [class.has-content]="hasContent">
      <ng-content></ng-content>
    </div>
  `
})
export class TabsComponent implements AfterViewInit {
  hasContent = false;

  @ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;

  ngAfterViewInit(): void {
    this.hasContent = this.tabs.length > 0; // Error: value changed after check
  }
}

Fixed: trigger change detection after the update:

import { ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'app-tabs',
  template: `
    <div [class.has-content]="hasContent">
      <ng-content></ng-content>
    </div>
  `
})
export class TabsComponent implements AfterViewInit {
  hasContent = false;

  @ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit(): void {
    this.hasContent = this.tabs.length > 0;
    this.cdr.detectChanges(); // Tells Angular to re-check this component
  }
}

detectChanges() triggers a synchronous change detection run for this component and its children. Angular performs a fresh check, so the second verification pass sees the already-updated value and does not throw.

Note: Use detectChanges() sparingly. If you find yourself calling it in many places, it often points to an architectural issue: consider restructuring your data flow instead.

Fix 3: Use setTimeout to Defer Updates

Wrapping the problematic update in setTimeout pushes it to the next macrotask, which triggers a new change detection cycle. The update no longer conflicts with the current check.

@Component({
  selector: 'app-dashboard',
  template: `<p>Status: {{ status }}</p>`
})
export class DashboardComponent implements AfterViewInit {
  status = 'loading';

  ngAfterViewInit(): void {
    setTimeout(() => {
      this.status = 'ready'; // Runs in a new change detection cycle
    });
  }
}

The setTimeout with no delay (or Promise.resolve().then(...)) defers execution until after the current change detection cycle completes. Angular’s zone.js patches setTimeout, so it automatically triggers a new round of change detection.

When to use this approach:

  • When detectChanges() causes cascading issues.
  • When you need to update a value after content projection or view initialization.
  • When dealing with third-party libraries that modify DOM outside Angular’s awareness.

This technique is well-known in other frameworks too. React developers face similar timing challenges; see preventing render-time state updates for the comparable pattern.

Fix 4: Fix Async Pipe Causing the Error

The async pipe is generally the recommended way to handle observables in templates. But it can trigger this error when the observable emits synchronously during change detection.

Broken: observable emits immediately on subscription:

@Component({
  selector: 'app-notification',
  template: `<div *ngIf="message$ | async as msg">{{ msg }}</div>`
})
export class NotificationComponent implements OnInit {
  message$!: Observable<string>;

  constructor(private notificationService: NotificationService) {}

  ngOnInit(): void {
    // BehaviorSubject emits its current value immediately
    // If parent changes this during its own check, error occurs
    this.message$ = this.notificationService.latestMessage$;
  }
}

Fixed: use shareReplay and ensure stable emission timing:

@Component({
  selector: 'app-notification',
  template: `<div *ngIf="message$ | async as msg">{{ msg }}</div>`
})
export class NotificationComponent implements OnInit {
  message$!: Observable<string>;

  constructor(private notificationService: NotificationService) {}

  ngOnInit(): void {
    this.message$ = this.notificationService.latestMessage$.pipe(
      observeOn(asyncScheduler), // Delays emission to next microtask
      shareReplay(1)
    );
  }
}

The observeOn(asyncScheduler) operator defers emission so it does not happen synchronously during change detection. Alternatively, if the observable comes from an HTTP call (which is inherently async), the error usually does not occur. The problem is specific to synchronous emissions like BehaviorSubject, of(), or startWith().

Another common cause: initializing the observable in ngAfterViewInit instead of ngOnInit. Always set up your observable references as early as possible, ideally in ngOnInit or at the field declaration level.

Fix 5: Fix Parent-Child Component Data Flow

This is one of the most common causes. A child component modifies a parent’s data during its own initialization, violating Angular’s unidirectional data flow.

Broken: child modifies parent state via a service:

// parent.component.ts
@Component({
  selector: 'app-parent',
  template: `
    <p>Count: {{ sharedService.count }}</p>
    <app-child></app-child>
  `
})
export class ParentComponent {
  constructor(public sharedService: SharedService) {}
}

// child.component.ts
@Component({
  selector: 'app-child',
  template: `<p>Child loaded</p>`
})
export class ChildComponent implements OnInit {
  constructor(private sharedService: SharedService) {}

  ngOnInit(): void {
    this.sharedService.count = 42; // Modifies parent's binding after parent was checked
  }
}

Fixed: use an event-based approach with proper timing:

// child.component.ts
@Component({
  selector: 'app-child',
  template: `<p>Child loaded</p>`
})
export class ChildComponent implements OnInit {
  @Output() initialized = new EventEmitter<number>();

  ngOnInit(): void {
    // Emit event instead of directly modifying shared state
    Promise.resolve().then(() => {
      this.initialized.emit(42);
    });
  }
}

// parent.component.ts
@Component({
  selector: 'app-parent',
  template: `
    <p>Count: {{ count }}</p>
    <app-child (initialized)="onChildInit($event)"></app-child>
  `
})
export class ParentComponent {
  count = 0;

  onChildInit(value: number): void {
    this.count = value;
  }
}

The key principle: data flows down from parent to child via @Input(), and events flow up from child to parent via @Output(). When a child needs to change something the parent displays, it should emit an event, not directly mutate shared state.

If you have worked with React, this is the same principle behind avoiding too many re-renders: state updates must happen at the right time and in the right direction.

Fix 6: Use OnPush Change Detection Strategy

Switching to OnPush change detection reduces the surface area for this error. With OnPush, Angular only checks the component when its @Input() references change, an event fires from the template, or you manually trigger detection.

@Component({
  selector: 'app-performance-list',
  template: `
    <ul>
      <li *ngFor="let item of items">{{ item.name }}</li>
    </ul>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class PerformanceListComponent {
  @Input() items: Item[] = [];
}

With OnPush, Angular will not re-check this component on every change detection cycle, only when items receives a new reference. This means accidental mutations to the same array reference will not trigger the error (or updates, which is the trade-off).

To update an OnPush component from within, use ChangeDetectorRef.markForCheck():

@Component({
  selector: 'app-live-feed',
  template: `<p>{{ latestUpdate }}</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class LiveFeedComponent implements OnInit, OnDestroy {
  latestUpdate = '';
  private destroy$ = new Subject<void>();

  constructor(
    private feedService: FeedService,
    private cdr: ChangeDetectorRef
  ) {}

  ngOnInit(): void {
    this.feedService.updates$
      .pipe(takeUntil(this.destroy$))
      .subscribe(update => {
        this.latestUpdate = update;
        this.cdr.markForCheck(); // Schedules a check for the next cycle
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

markForCheck() is different from detectChanges(). It does not trigger an immediate check; it marks the component (and its ancestors) as dirty, so Angular will check it during the next change detection cycle. This avoids the timing conflict that causes ExpressionChangedAfterItHasBeenCheckedError.

In my own teams I default every new component to OnPush and require a documented reason to switch back to Default. It is not only a fix for this error; it is the cheapest performance win Angular offers, and it prevents a whole category of timing bugs from ever shipping. The transition cost is real (you have to think about input references), but the cost of NOT doing it is the kind of debugging that ate my week.

Fix 7: Fix ngAfterViewInit vs ngAfterContentInit Timing

These two lifecycle hooks run at different points, and using the wrong one is a frequent cause of this error.

  • ngAfterContentInit runs after Angular projects content into the component (via <ng-content>). It runs before the parent’s change detection check completes.
  • ngAfterViewInit runs after Angular initializes the component’s view and child views. It runs after the parent’s change detection check completes.

If you modify a bound property in ngAfterViewInit, the parent has already been checked, so the error fires. But the same modification in ngAfterContentInit may work fine.

Broken: updating state in ngAfterViewInit:

@Component({
  selector: 'app-form-wrapper',
  template: `
    <div [class.valid]="isFormValid">
      <app-dynamic-form #form></app-dynamic-form>
    </div>
  `
})
export class FormWrapperComponent implements AfterViewInit {
  isFormValid = false;

  @ViewChild('form') formComponent!: DynamicFormComponent;

  ngAfterViewInit(): void {
    this.isFormValid = this.formComponent.form.valid; // Too late — parent already checked
  }
}

Fixed: subscribe to changes instead of reading synchronously:

@Component({
  selector: 'app-form-wrapper',
  template: `
    <div [class.valid]="isFormValid">
      <app-dynamic-form #form></app-dynamic-form>
    </div>
  `
})
export class FormWrapperComponent implements AfterViewInit {
  isFormValid = false;

  @ViewChild('form') formComponent!: DynamicFormComponent;

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit(): void {
    // Subscribe to form status changes — each emission triggers a new cycle
    this.formComponent.form.statusChanges.subscribe(status => {
      this.isFormValid = status === 'VALID';
      this.cdr.markForCheck();
    });

    // Handle the initial state
    this.isFormValid = this.formComponent.form.valid;
    this.cdr.detectChanges();
  }
}

When you need to read from @ViewChild references, you have no choice but to use ngAfterViewInit. The key is to follow up with detectChanges() for the initial value, then use reactive patterns (subscriptions, observables) for subsequent updates.

If your logic only depends on projected content (not view children), move it to ngAfterContentInit instead; it runs earlier and avoids the issue entirely.

Fix 8: Use BehaviorSubject for Reactive Data Flow

Getters and computed properties are a hidden source of this error. If a getter returns a new object or array reference on every call, Angular sees a different value on each check.

Broken: getter creates a new array every time:

@Component({
  selector: 'app-filtered-list',
  template: `
    <ul>
      <li *ngFor="let item of activeItems">{{ item.name }}</li>
    </ul>
  `
})
export class FilteredListComponent {
  items: Item[] = [];

  get activeItems(): Item[] {
    return this.items.filter(i => i.active); // New array reference every call
  }
}

Fixed: use a BehaviorSubject to manage the derived state:

@Component({
  selector: 'app-filtered-list',
  template: `
    <ul>
      <li *ngFor="let item of activeItems$ | async">{{ item.name }}</li>
    </ul>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class FilteredListComponent implements OnInit {
  private itemsSubject = new BehaviorSubject<Item[]>([]);
  activeItems$!: Observable<Item[]>;

  ngOnInit(): void {
    this.activeItems$ = this.itemsSubject.pipe(
      map(items => items.filter(i => i.active)),
      distinctUntilChanged((prev, curr) =>
        prev.length === curr.length && prev.every((item, i) => item === curr[i])
      )
    );
  }

  updateItems(newItems: Item[]): void {
    this.itemsSubject.next(newItems);
  }
}

The BehaviorSubject combined with distinctUntilChanged ensures Angular only receives a new reference when the data actually changes. The async pipe handles subscription and unsubscription automatically.

This pattern scales well. For complex state management, consider a dedicated state management library like NgRx or Akita, which enforce immutable data patterns and eliminate this class of errors entirely.

You can also fix the simpler cases without a BehaviorSubject by caching the computed value:

export class FilteredListComponent implements OnChanges {
  @Input() items: Item[] = [];
  activeItems: Item[] = [];

  ngOnChanges(): void {
    this.activeItems = this.items.filter(i => i.active); // Computed once per change, not per check
  }
}

This approach works well when the data comes from an @Input(). The ngOnChanges hook runs once when the input changes, producing a stable reference that Angular can check safely. The same principle applies to any derived state: compute once when the source changes, not on every read.

Edge Cases That Still Trip Me Up

If none of the fixes above solved your issue, here are the less common causes I have personally hit:

Getter with Date or Math.random():

// This will ALWAYS trigger the error
get timestamp(): string {
  return new Date().toISOString(); // Different value every millisecond
}

Cache the value instead:

timestamp: string = new Date().toISOString();

Third-party library modifying the DOM outside Angular:

Some libraries (jQuery plugins, D3, chart libraries) modify the DOM directly. Wrap their callbacks in NgZone.run():

constructor(private ngZone: NgZone) {}

ngAfterViewInit(): void {
  this.chart = new ThirdPartyChart(this.chartEl.nativeElement);
  this.chart.onReady(() => {
    this.ngZone.run(() => {
      this.isChartReady = true;
    });
  });
}

Running outside Angular’s zone accidentally:

If code runs outside NgZone (via runOutsideAngular), changes will not trigger change detection. When you later re-enter the zone, the accumulated changes can cause the error. Make sure zone transitions are intentional and managed.

Structural directives with dynamic conditions:

Using *ngIf or *ngSwitch with expressions that evaluate differently during the verification check will trigger the error. Ensure the condition resolves to a stable value before change detection runs.

Debug technique, find the exact binding:

The error message tells you the previous and current values. To find which binding caused it, add ng.getComponent(element) in the browser console and inspect the component’s properties. In Angular 9+, enable detailed error messages:

// main.ts (development only)
import { enableProdMode } from '@angular/core';

// Do NOT call enableProdMode() during debugging; the error
// only shows in dev mode and it is your best diagnostic tool.

Check the full stack trace in the console. It shows the exact template expression and component where the mismatch occurred. Follow the chain from the point of change back to the source of the mutation.

Check for @HostBinding properties driven by getters. A @HostBinding('class.active') that calls a getter behaves the same as a template getter; it runs on every check. If the getter computes from time, randomness, or freshly filtered arrays, you get the error on every cycle. Cache the value on a field and update it only when the source changes.

Look for markAsDirty / markAsTouched called during view init. Reactive Forms validators that call control.markAsTouched() or control.updateValueAndValidity() inside ngAfterViewInit push a status change after the parent has been checked. Wrap the call in Promise.resolve().then(...) or move it into a valueChanges subscription so it runs in a new cycle.

Inspect content-projected components. If you have <ng-content> and the projected component sets a property the host reads via @ContentChild, the read happens in ngAfterContentChecked, which fires before the host’s view check. The fix is to subscribe to the child’s EventEmitter instead of reading the property synchronously.

Rule out Angular Signals interop. In Angular 16+, mixing Signals with zone-based change detection can trip this error when a signal updates inside effect() that runs during a check. Move signal writes out of effect() callbacks that depend on bound state, or convert the consuming component to fully signal-based with OnPush and toSignal.

Audit your *ngIf and *ngFor conditions. Conditions that reference Date.now(), Math.random(), or any external mutable singleton will return a different value on the verification pass. Make the condition reference only fields and inputs that change through Angular’s normal flow.

What Other Tutorials Get Wrong About This Error

Most Angular tutorials list the same fixes but frame them in ways that produce subtle bugs.

They recommend setTimeout(() => ..., 0) as the first fix. This is the worst possible default. setTimeout defers the change to the next cycle, which suppresses the error but does not address the timing bug. A setTimeout in production code is almost always a sign of unresolved design debt; the durable fix is to restructure data flow.

They show ChangeDetectorRef.detectChanges() without warning about cascade. Calling detectChanges() triggers a fresh synchronous check for this component and its children. If multiple components in the tree do this on init, you can produce noticeable jank. Use markForCheck() (which schedules a check for the next cycle) rather than detectChanges() unless you specifically need synchronous re-check.

They miss the getter / pipe referential-equality cause. A getter that returns this.items.filter(...) creates a new array reference on every call. Angular sees a different value on the second pass and throws. Many tutorials list this as a side note; in my experience it is the most common single cause after lifecycle-hook mistakes.

They confuse production “fix” with actual fix. Production builds skip the second check, so the error message disappears. Tutorials that say “this error only happens in dev, ignore it in prod” miss that the underlying timing issue still produces stale data, visual glitches, and inconsistent state, they are just invisible.

They skip the Signals interop case. Angular 16+ introduces Signals, and mixing Signals with zone-based change detection can re-introduce this exact error in new shapes. Articles written before mid-2023 are silent on this and miss a growing class of failures.

They omit OnPush as a preventive design. OnPush change detection prevents most of these errors at the architectural level by reducing how often Angular checks. Articles that present OnPush as “only for performance” miss its role as the cheapest possible defense against this entire error class.

Frequently Asked Questions

Why does this error only appear in development mode?

Angular’s dev mode runs change detection twice on each cycle: once to compute and apply values, and a second time to verify stability. If a binding value differs between the two runs, Angular throws to surface the bug. Production builds skip the second run for performance, so the error message disappears even though the underlying timing issue is still there.

Is it safe to ignore this error in production?

No. Production hides the error message but not the cause. The underlying data is still changing at the wrong time, which produces stale renders, missed updates, and intermittent visual glitches. The dev-mode error is a diagnostic tool, not the bug itself. Fix the cause; do not just deploy to production to silence the message.

When should I use detectChanges() vs markForCheck()?

detectChanges() runs change detection synchronously for the current component and its children right now. Use it when you must propagate a change to the DOM before the function returns (e.g., before reading a measured DOM value). markForCheck() schedules the component for the next change detection cycle without running it immediately. Use it from inside an OnPush component when an async event changes state; the next zone tick will pick it up. For most cases, prefer markForCheck(); reach for detectChanges() only when timing matters synchronously.

Why does my BehaviorSubject cause this error?

BehaviorSubject emits its current value synchronously on subscription. If you subscribe inside ngOnInit of a child component, the emission can land during the parent’s change detection cycle, mutating something the parent already checked. The fix is observeOn(asyncScheduler) to defer the emission, or setting up the subject in the parent and passing values down via @Input().

Does OnPush change detection prevent this error entirely?

Mostly, but not absolutely. OnPush reduces how often Angular runs checks on a component, which shrinks the surface area for the timing mismatch. You can still hit the error inside an OnPush component if you call detectChanges() or markForCheck() at the wrong moment, or if you mutate an @Input() value (instead of producing a new reference) in a way that bypasses the change-detection optimization. As a baseline though, OnPush cuts these errors dramatically.

How do I find which exact binding caused the error?

The error message names the previous and current values: Previous value: 'false'. Current value: 'true'. Combine that with the stack trace, which in Angular 9+ includes the template line number. In DevTools, expand the error frame; the first frame inside your component points to the binding. If the binding is set indirectly (via a service or observable), search the codebase for assignments to the property name in the error message.

If you are dealing with other Angular injection issues alongside this error, check how to fix NullInjectorError for related dependency injection problems that can compound with change detection timing issues.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles