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:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError:
Expression has changed after it was checked.
Previous value: 'loading'. Current value: 'ready'.Console detail:
ERROR CONTEXT DebugContext {
component: AppComponent,
nodeIndex: 5,
bindingIndex: 0,
property: 'status'
}Component location:
Error occurs in AppComponent at bindings:
- {{ status }}
- [class.loading]="isLoading"Common Causes
- 1.Parent modifies child input - Parent changes child's @Input during change detection
- 2.Lifecycle hook modifies state - ngOnInit/ngOnChanges changes binding value
- 3.ViewChild accessed too early - Reading ViewChild before ngAfterViewInit
- 4.Event handler modifies binding - Function in template changes value
- 5.Async operation synchronous - Promise/Observable resolved in constructor
- 6.Getter returning new object - Getter creates new reference each call
- 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
| Cause | Solution |
|---|---|
| ngOnInit modifies binding | Move to constructor or defer |
| Parent changes child input | Use ngOnChanges or getter |
| ViewChild before init | Use ngAfterViewInit + setTimeout |
| Input setter side effects | Use ngOnChanges |
| Getter returns new object | Cache result or use pure pipe |
| Function in template | Use 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
@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
Related Issues
- [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)
Related Articles
- [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>