Fix: Angular ExpressionChangedAfterItHasBeenCheckedError
Quick Answer
How to fix ExpressionChangedAfterItHasBeenCheckedError in Angular caused by change detection timing issues, lifecycle hooks, async pipes, and parent-child data flow.
The Error
You run your Angular application in development mode and see this in the console:
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.
Why This Happens
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:
- Angular triggers change detection (e.g., after an event, HTTP response, or timer).
- Angular checks all template bindings and updates the DOM.
- In dev mode only, Angular runs a second check to verify stability.
- 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
ngAfterViewInitchanges 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.
Common Mistake: Many developers suppress this error with
setTimeoutordetectChanges()without understanding the root cause. These are valid solutions in specific cases, but blindly applying them can mask real architectural problems. Always understand why the value is changing before choosing a fix.
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 fixing infinite loops in useEffect and preventing render-time state updates for comparable patterns.
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.
Pro Tip:
OnPushis not just a fix for this error — it is a performance best practice. Components that only depend on their inputs and internal events should always useOnPush. It reduces unnecessary change detection cycles across your entire app and prevents a whole category of timing bugs. Start new components withOnPushby default and only switch toDefaultwhen you have a specific reason.
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.
ngAfterContentInitruns after Angular projects content into the component (via<ng-content>). It runs before the parent’s change detection check completes.ngAfterViewInitruns 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. This pattern is similar to how React developers handle derived state carefully to avoid infinite re-renders.
Still Not Working?
If none of the fixes above solved your issue, check these less common causes:
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.
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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Angular NullInjectorError: No provider for X
How to fix Angular NullInjectorError No provider for service caused by missing providers, wrong module imports, standalone components, and lazy-loaded module issues.
Fix: TypeError: x is not a function
How to fix JavaScript TypeError is not a function caused by wrong variable types, missing imports, overwritten variables, incorrect method names, and callback issues.
Fix: TypeScript Argument of type 'X' is not assignable to parameter of type 'Y' (TS2345)
How to fix TypeScript error TS2345 Argument of type is not assignable to parameter of type, covering null narrowing, union types, generics, callback types, type widening, enums, and React event handlers.
Fix: TypeScript Could not find a declaration file for module (TS7016)
How to fix the TypeScript TS7016 error 'Could not find a declaration file for module' by installing @types packages, creating declaration files, and configuring tsconfig.json.