The Problem

When you subscribe to a FormControl's valueChanges observable and then programmatically update the same control's value, the update triggers valueChanges again, creating an infinite loop.

Symptoms

  • Browser freezes or crashes
  • Maximum call stack size exceeded error
  • Console fills with repeated log messages
  • Form validation runs hundreds of times per second

Real Error Scenario

typescript
// Component
this.form.get('email').valueChanges.subscribe(value => {
  if (value && !value.includes('@')) {
    this.form.get('email').setValue(value + '@example.com'); // TRIGGERS valueChanges AGAIN!
  }
});

Every setValue call fires valueChanges, which calls setValue again, creating an infinite recursion.

How to Fix It

Fix 1: Use emitEvent: false

typescript
this.form.get('email').valueChanges.subscribe(value => {
  if (value && !value.includes('@')) {
    this.form.get('email').setValue(value + '@example.com', { emitEvent: false });
  }
});

emitEvent: false updates the value without triggering valueChanges.

Fix 2: Use patchValue with emitEvent

typescript
this.form.get('address').valueChanges.subscribe(addr => {
  if (addr && addr.zipCode && !addr.city) {
    this.form.patchValue({
      city: this.lookupCity(addr.zipCode)
    }, { emitEvent: false });
  }
});

Fix 3: Use distinctUntilChanged

typescript
this.form.get('email').valueChanges
  .pipe(distinctUntilChanged())
  .subscribe(value => {
    if (value && !value.includes('@')) {
      this.form.get('email').setValue(value + '@example.com', { emitEvent: false });
    }
  });

Fix 4: Use takeUntil for Cleanup

```typescript private destroy$ = new Subject<void>();

ngOnInit() { this.form.get('email').valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => { this.form.get('email').setValue(this.normalize(value), { emitEvent: false }); }); }

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

Fix 5: Custom Validator Instead of valueChanges

```typescript // Better approach: use a validator instead of valueChanges function emailDomainValidator(control: AbstractControl): ValidationErrors | null { const value = control.value; if (value && !value.includes('@')) { return { invalidEmail: true }; } return null; }

this.form = this.fb.group({ email: ['', [Validators.required, emailDomainValidator]] }); ```