Introduction

Angular runs change detection in development mode twice to catch errors. If a binding value changes between the first check and the second check, Angular throws ExpressionChangedAfterItHasBeenCheckedError, indicating a state change during change detection.

Symptoms

ExpressionChangedAfterItHasBeenCheckedError:

typescript
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: 
Expression has changed after it was checked. 
Previous value: 'loading'. Current value: 'ready'.

Console detail:

bash
ERROR CONTEXT DebugContext {
  component: AppComponent,
  nodeIndex: 5,
  bindingIndex: 0,
  property: 'status'
}

Component location:

bash
Error occurs in AppComponent at bindings:
- {{ status }}
- [class.loading]="isLoading"

Common Causes

  1. 1.Parent modifies child input - Parent changes child's @Input during change detection
  2. 2.Lifecycle hook modifies state - ngOnInit/ngOnChanges changes binding value
  3. 3.ViewChild accessed too early - Reading ViewChild before ngAfterViewInit
  4. 4.Event handler modifies binding - Function in template changes value
  5. 5.Async operation synchronous - Promise/Observable resolved in constructor
  6. 6.Getter returning new object - Getter creates new reference each call
  7. 7.Input setter has side effects - @Input setter modifies other properties

Step-by-Step Fix

Step 1: Identify the Problem Component

```typescript // Error shows which component and binding ExpressionChangedAfterItHasBeenCheckedError: Expression has changed... at checkView (core.js:123) at AppComponent.status // Problem component and property

// Check the component @Component({ template: <div>{{ status }}</div> }) export class AppComponent { status = 'loading';

ngOnInit() { this.status = 'ready'; // Changes during first detection cycle } } ```

Step 2: Fix ngOnInit/ngOnChanges Modifications

typescript // BAD: Modifying binding in ngOnInit @Component({ template: <div>{{ status }}</div>` }) export class AppComponent implements OnInit { status = 'loading';

ngOnInit() { this.status = 'ready'; // Changes during check! } }

// FIX: Initialize in constructor export class AppComponent { status = 'ready'; // Set in constructor/field

// No modification in ngOnInit ngOnInit() { // Only non-binding operations here } }

// FIX: Use setTimeout to defer ngOnInit() { setTimeout(() => { this.status = 'ready'; // Deferred to next cycle }); }

// FIX: Use ChangeDetectorRef.detectChanges constructor(private cdr: ChangeDetectorRef) {}

ngOnInit() { this.status = 'ready'; this.cdr.detectChanges(); // Trigger extra detection } ```

Step 3: Fix Parent-Child Input Changes

typescript // BAD: Parent modifies child input during detection @Component({ template: <child [data]="processData()"></child>` }) export class ParentComponent { rawData = { value: 1 };

processData() { this.rawData.value++; // Side effect! return this.rawData; } }

// FIX: Use pure computation @Component({ template: <child [data]="processedData"></child> }) export class ParentComponent { rawData = { value: 1 }; processedData = { value: 1 };

ngOnInit() { // Process once, outside detection this.processedData = { ...this.rawData, value: this.rawData.value + 1 }; } }

// FIX: Use getter without side effects get processedData() { return { ...this.rawData }; // Return copy, don't modify original } ```

Step 4: Fix ViewChild Access Timing

typescript // BAD: Access ViewChild before ngAfterViewInit @Component({ template: <child #child></child>` }) export class AppComponent implements OnInit { @ViewChild('child') child: ChildComponent;

ngOnInit() { console.log(this.child.value); // child is undefined! // Or modifying child causes error this.child.value = 'new'; } }

// FIX: Access in ngAfterViewInit export class AppComponent implements AfterViewInit { @ViewChild('child') child: ChildComponent;

ngAfterViewInit() { // Safe to access now console.log(this.child.value);

// If modifying, defer with setTimeout setTimeout(() => { this.child.value = 'new'; }); } } ```

Step 5: Fix Input Setter Side Effects

typescript // BAD: Input setter modifies other properties @Component({ template: <div>{{ count }}</div> <div>{{ doubleCount }}</div> ` }) export class ChildComponent { @Input() count = 0; doubleCount = 0;

@Input() set count(value: number) { this._count = value; this.doubleCount = value * 2; // Side effect! } }

// FIX: Use ngOnChanges export class ChildComponent implements OnChanges { @Input() count = 0; doubleCount = 0;

ngOnChanges(changes: SimpleChanges) { if (changes.count) { this.doubleCount = changes.count.currentValue * 2; } } }

// FIX: Use getter without modifying state export class ChildComponent { @Input() count = 0;

get doubleCount(): number { return this.count * 2; // Pure computation } } ```

Step 6: Fix Getter Impurity

typescript // BAD: Getter creates new object each call @Component({ template: <div>{{ items.length }}</div>` }) export class AppComponent { data = [1, 2, 3];

get items() { return this.data.filter(x => x > 1); // New array each call! } }

// FIX: Cache the result export class AppComponent { data = [1, 2, 3]; filteredItems = [2, 3];

ngOnInit() { this.filteredItems = this.data.filter(x => x > 1); }

// Update when data changes addItem(item: number) { this.data.push(item); this.filteredItems = this.data.filter(x => x > 1); } }

// FIX: Use pure pipe @Pipe({ name: 'filter', pure: true }) export class FilterPipe implements PipeTransform { transform(items: number[], threshold: number): number[] { return items.filter(x => x > threshold); } }

// Template: {{ data | filter:1 }} ```

Step 7: Use setTimeout for Deferral

```typescript // Defer state change to next change detection cycle ngOnInit() { // BAD: Immediate change this.status = 'ready';

// FIX: Deferred change setTimeout(() => { this.status = 'ready'; });

// Or setTimeout with 0 setTimeout(() => { this.status = 'ready'; }, 0); }

// For multiple changes ngOnInit() { Promise.resolve().then(() => { this.status = 'ready'; this.loading = false; }); } ```

Step 8: Use ChangeDetectorRef

typescript // Manually control change detection @Component({ template: <div>{{ status }}</div>` }) export class AppComponent implements OnInit { status = 'loading';

constructor(private cdr: ChangeDetectorRef) {}

ngOnInit() { this.status = 'ready'; this.cdr.detectChanges(); // Run detection again } }

// Or detach and reattach ngOnInit() { this.cdr.detach(); // Stop automatic detection this.status = 'ready'; this.cdr.reattach(); // Resume automatic detection }

// Or checkNoChanges (verify) ngAfterViewInit() { this.cdr.checkNoChanges(); // Verify no changes } ```

Step 9: Use OnPush Strategy

typescript // Reduce change detection cycles @Component({ template: <div>{{ status }}</div>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class AppComponent implements OnChanges { @Input() data: any; status = 'loading';

ngOnChanges(changes: SimpleChanges) { if (changes.data) { this.status = 'ready'; // OnPush only runs on @Input change } } }

// OnPush reduces but doesn't eliminate error // Still need to follow patterns above ```

Step 10: Debug with Enhanced Error

```typescript // Enable detailed error logging // In polyfills.ts or main.ts import { enableDebugTools } from '@angular/platform-browser';

platformBrowserDynamic().bootstrapModule(AppModule) .then(moduleRef => { const appRef = moduleRef.injector.get(ApplicationRef); const componentRef = appRef.components[0]; enableDebugTools(componentRef); });

// In browser console: ng.profiler.timeChangeDetection({ record: true });

// Shows all bindings that changed ```

Error Resolution Patterns

CauseSolution
ngOnInit modifies bindingMove to constructor or defer
Parent changes child inputUse ngOnChanges or getter
ViewChild before initUse ngAfterViewInit + setTimeout
Input setter side effectsUse ngOnChanges
Getter returns new objectCache result or use pure pipe
Function in templateUse property, update on event

Verification

```typescript // After fixing the error

// 1. Run in development mode ng serve

// Should NOT see ExpressionChangedAfterItHasBeenCheckedError

// 2. Test component lifecycle // Add console logs ngOnInit() { console.log('ngOnInit: status=', this.status); }

ngAfterViewInit() { console.log('ngAfterViewInit: status=', this.status); }

// Verify logs show stable values

// 3. Run with strict mode ng build --strictTemplates

// Should build without errors

// 4. Production build ng build --prod

// Should succeed (prod mode has single detection)

// 5. Check Angular DevTools // Open DevTools // Verify component tree stable // No flashing bindings ```

Prevention

To prevent Angular ExpressionChangedAfterItHasBeenCheckedError issues from recurring, implement these proactive measures:

1. Enable Strict Template Checking

```json // tsconfig.json { "angularCompilerOptions": { "strictTemplates": true } }

// Catches many issues at compile time ```

2. Follow Single Responsibility for Bindings

```typescript // Avoid: Modifying bound values in lifecycle hooks // GOOD: Initialize in constructor or use ngOnChanges

@Component({ template: <div>{{ status }}</div> }) export class GoodComponent { status = 'loading'; // Initialize at declaration

// Or in constructor constructor() { this.status = 'ready'; } } ```

3. Use OnPush Change Detection

typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `...`
})
export class OptimizedComponent {
  // Reduces change detection cycles
  // Less likely to trigger the error
}

Best Practices Checklist

  • [ ] Enable strict template checking
  • [ ] Initialize bound values in constructor
  • [ ] Use OnPush change detection
  • [ ] Avoid function calls in templates
  • [ ] Use ngAfterViewInit for ViewChild
  • [ ] Test in development mode
  • [Fix Angular Change Detection Loop](/articles/fix-angular-change-detection-loop)
  • [Fix Angular Dependency Injection Error](/articles/fix-angular-dependency-injection-error)
  • [Fix Angular Memory Leak Subscription](/articles/fix-angular-memory-leak-subscription)
  • [Technical troubleshooting: Fix Browser Back Button State Lost Spa Navigation ](browser-back-button-state-lost-spa-navigation)
  • [Fix Cors Fetch Localhost Development Blocked Issue in Frontend](cors-fetch-localhost-development-blocked)
  • [Fix Csp Violation Blocked Inline Script Style Issue in Frontend](csp-violation-blocked-inline-script-style)
  • [Fix Angular Change Detection Loop](fix-angular-change-detection-loop)
  • [Fix Angular Dependency Injection Error](fix-angular-dependency-injection-error)

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "Fix Angular ExpressionChangedAfterItHasBeenCheckedError", "description": "Troubleshoot ExpressionChangedAfterItHasBeenCheckedError. Fix binding side effects, use ngOnChanges, defer updates.", "url": "https://www.fixwikihub.com/fix-angular-expressionchangedafterchecked", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-04-03T23:47:29.095Z", "dateModified": "2026-04-03T23:47:29.095Z" } </script>