Validate Against the Server (Debounced)
Async Validators
An async validator returns a Promise or Observable of ValidationErrors, letting you check things only the server knows — like email uniqueness.
What you'll learn
- Write an AsyncValidatorFn
- Apply it as the third FormControl argument
- Combine with debounce for fewer requests
Some checks require a round trip — is this username available, does this discount code exist, is this email already registered. Async validators are how Angular models that.
The Signature
An async validator returns a Promise or Observable that emits ValidationErrors | null and then completes.
import { AsyncValidatorFn } from '@angular/forms';
import { inject } from '@angular/core';
import { map } from 'rxjs';
import { ApiService } from './api.service';
export function emailTaken(): AsyncValidatorFn {
const api = inject(ApiService);
return (c) =>
api.checkEmail(c.value).pipe(
map((taken) => (taken ? { taken: true } : null)),
);
} Wiring It Up
Async validators go in the third argument of FormControl, after the sync validators.
import { FormControl, Validators } from '@angular/forms';
const email = new FormControl(
'',
{ validators: [Validators.required, Validators.email] },
);
// Or with the array form:
const email2 = new FormControl(
'',
[Validators.required, Validators.email],
[emailTaken()],
); While the request is in flight, control.status is 'PENDING'. Use that to show a spinner.
Debouncing
Angular runs async validators on every value change by default. That hammers your server. The fix is to opt out of the default and run a manual pipeline.
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
email.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((v) => api.checkEmail(v)),
)
.subscribe((taken) => {
email.setErrors(taken ? { taken: true } : null);
}); That manual approach gives you control over when the request fires. For most cases the built-in async validator is fine — just keep an eye on request volume.
FormArray — Dynamic Lists →