Angular
Angular2
input validation
debounce time
frontend development

How to trigger validation input after debounce time in Angular2?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Running validation on every keypress can make a form feel jumpy and can overwhelm an API if the validation depends on the server. In Angular, the common fix is to debounce the control's value stream so validation runs only after the user pauses typing.

Debouncing a Reactive Form Control

Reactive forms expose a valueChanges observable for each control. That stream is the natural place to add debounceTime, skip duplicate values, and trigger validation logic only when the user has paused.

typescript
1import { Component, OnDestroy, OnInit } from '@angular/core';
2import { FormControl, Validators } from '@angular/forms';
3import { Subject, of } from 'rxjs';
4import {
5  debounceTime,
6  distinctUntilChanged,
7  switchMap,
8  takeUntil,
9  map,
10  catchError,
11  finalize,
12  tap
13} from 'rxjs/operators';
14import { HttpClient } from '@angular/common/http';
15
16@Component({
17  selector: 'app-signup',
18  templateUrl: './signup.component.html'
19})
20export class SignupComponent implements OnInit, OnDestroy {
21  username = new FormControl('', [Validators.required, Validators.minLength(3)]);
22  checking = false;
23  private destroy$ = new Subject<void>();
24
25  constructor(private http: HttpClient) {}
26
27  ngOnInit(): void {
28    this.username.valueChanges
29      .pipe(
30        debounceTime(400),
31        distinctUntilChanged(),
32        tap(() => {
33          this.checking = true;
34          this.clearUnavailableError();
35        }),
36        switchMap(value =>
37          this.checkUsername(String(value ?? '')).pipe(
38            finalize(() => {
39              this.checking = false;
40            })
41          )
42        ),
43        takeUntil(this.destroy$)
44      )
45      .subscribe(isAvailable => {
46        if (!isAvailable) {
47          this.username.setErrors({
48            ...(this.username.errors ?? {}),
49            unavailable: true
50          });
51        }
52      });
53  }
54
55  ngOnDestroy(): void {
56    this.destroy$.next();
57    this.destroy$.complete();
58  }
59
60  private checkUsername(value: string) {
61    if (value.trim().length < 3) {
62      return of(true);
63    }
64
65    return this.http
66      .get<{ available: boolean }>(`/api/users/available?username=${encodeURIComponent(value)}`)
67      .pipe(
68        map(response => response.available),
69        catchError(() => of(true))
70      );
71  }
72
73  private clearUnavailableError(): void {
74    const errors = this.username.errors;
75    if (!errors || !errors['unavailable']) {
76      return;
77    }
78
79    const nextErrors = { ...errors };
80    delete nextErrors['unavailable'];
81    this.username.setErrors(Object.keys(nextErrors).length ? nextErrors : null);
82  }
83}

This waits 400 milliseconds after the last keystroke, ignores repeated values, and checks only the latest input.

Why switchMap Matters

The validation is not just delayed; it also needs to be correct when requests overlap. switchMap is important because it cancels the previous request when a new value arrives.

Without switchMap, a slower response for an older value can arrive after a newer response and overwrite the correct validation state. That produces confusing UI where the error message does not match the current input.

For live validation, only the latest value matters, so switchMap is usually the right operator.

Using an Async Validator Instead

If you want the debounce logic attached directly to the form control, you can write a custom async validator. This makes the form setup more declarative.

typescript
1import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
2import { HttpClient } from '@angular/common/http';
3import { Observable, of, timer } from 'rxjs';
4import { catchError, map, switchMap } from 'rxjs/operators';
5
6export function usernameAvailableValidator(http: HttpClient): AsyncValidatorFn {
7  return (control: AbstractControl): Observable<ValidationErrors | null> => {
8    const value = String(control.value ?? '').trim();
9
10    if (value.length < 3) {
11      return of(null);
12    }
13
14    return timer(400).pipe(
15      switchMap(() =>
16        http.get<{ available: boolean }>(
17          `/api/users/available?username=${encodeURIComponent(value)}`
18        )
19      ),
20      map(response => (response.available ? null : { unavailable: true })),
21      catchError(() => of(null))
22    );
23  };
24}

This works well when you want the validator to be reusable across multiple forms.

Showing Feedback in the Template

Debouncing the request is only part of the user experience. Error messages should usually appear after the user has interacted with the field.

html
1<input [formControl]="username" />
2
3<div *ngIf="checking">Checking availability...</div>
4<div *ngIf="username.touched && username.hasError('required')">
5  Username is required.
6</div>
7<div *ngIf="username.touched && username.hasError('minlength')">
8  Use at least 3 characters.
9</div>
10<div *ngIf="username.touched && username.hasError('unavailable')">
11  Username is already taken.
12</div>

Using touched or dirty avoids showing errors immediately when the form first renders.

Common Pitfalls

A common mistake is validating on every keypress with no debounce at all. That can flood the backend and make loading indicators flash constantly.

Another issue is using the wrong flattening operator. If older responses are still allowed to update the form, the validation state can drift away from the latest text.

Be careful when clearing errors. Calling setErrors(null) blindly removes every error, including built-in validation such as required or minlength.

Finally, if you subscribe to valueChanges manually, remember to clean up the subscription when the component is destroyed.

Summary

  • Use valueChanges with debounceTime to delay validation until typing pauses.
  • Prefer switchMap so stale requests cannot overwrite new results.
  • Async validators are a good option when you want reusable declarative validation.
  • Show messages only after user interaction to keep forms readable.
  • Clear custom errors carefully so other validators continue to work.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.