June 29, 2026

Angular Reactive Forms Validation — Complete Guide (2026)

New complete guide — June 29, 2026 · Reactive forms validation with Angular 19. Angular tutorials hub

Kindson Munonye · Software engineer & technical author
GitHub · LinkedIn · About · YouTube
Last updated by Kindson Munonye — June 29, 2026

Last updated: June 29, 2026 · Estimated time: ~50 minutes · Try the StackBlitz demo →

This is the definitive guide to validation in Angular reactive forms. It combines and extends our Reactive Forms tutorial and Form Validation guide with Angular 19 patterns, custom validators, async checks, and production-ready error handling.

Prerequisites

Step 1 — Project setup (standalone component)

ng new validation-demo --routing --style=scss --ssr=false
cd validation-demo
# Add ReactiveFormsModule imports in standalone components
// signup.component.ts
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-signup',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './signup.component.html'
})
export class SignupComponent {
  private fb = inject(FormBuilder);
  form = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
    confirmPassword: ['', Validators.required]
  });
}

Step 2 — Built-in validators

ValidatorUse case
Validators.requiredNon-empty value
Validators.emailEmail format
Validators.minLength(n)Minimum characters
Validators.pattern(regex)Phone, username format
Validators.min/maxNumeric ranges

Step 3 — Display validation errors in the template

<input formControlName="email" class="form-control">
<span class="error" *ngIf="form.get('email')?.invalid && form.get('email')?.touched">
  <ng-container *ngIf="form.get('email')?.errors?.['required']">Email is required</ng-container>
  <ng-container *ngIf="form.get('email')?.errors?.['email']">Enter a valid email</ng-container>
</span>

Tip: Use touched or dirty to avoid showing errors on page load. On submit, call this.form.markAllAsTouched().

Step 4 — Custom validators

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function noWhitespace(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const v = (control.value || '').trim();
    return v.length ? null : { whitespace: true };
  };
}

export function forbiddenValue(forbidden: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null =>
    control.value === forbidden ? { forbiddenValue: true } : null;
}
username: ['', [Validators.required, noWhitespace(), forbiddenValue('admin')]]

Step 5 — Cross-field validation (password match)

function passwordMatch(group: AbstractControl): ValidationErrors | null {
  const pass = group.get('password')?.value;
  const confirm = group.get('confirmPassword')?.value;
  return pass === confirm ? null : { passwordMismatch: true };
}

form = this.fb.group({
  password: ['', [Validators.required, Validators.minLength(8)]],
  confirmPassword: ['', Validators.required]
}, { validators: passwordMatch });
<span *ngIf="form.errors?.['passwordMismatch'] && form.get('confirmPassword')?.touched">
  Passwords do not match
</span>

Step 6 — Async validators (username availability)

import { AsyncValidatorFn } from '@angular/forms';
import { map, delay } from 'rxjs/operators';
import { of } from 'rxjs';

export function usernameTaken(): AsyncValidatorFn {
  return (control) =>
    of(control.value === 'taken').pipe(
      delay(400),
      map(isTaken => (isTaken ? { usernameTaken: true } : null))
    );
}

username: ['', [Validators.required], [usernameTaken()]]
<span *ngIf="form.get('username')?.pending">Checking availability…</span>

Step 7 — Validate FormArray (dynamic fields)

skills = this.fb.array([
  this.fb.control('', Validators.required)
]);

addSkill() {
  this.skills.push(this.fb.control('', Validators.required));
}

get skillsValid() {
  return this.skills.controls.every(c => c.valid);
}

Step 8 — Dynamic validators with setValidators

onAccountTypeChange(type: 'personal' | 'business') {
  const company = this.form.get('companyName');
  if (type === 'business') {
    company?.setValidators([Validators.required]);
  } else {
    company?.clearValidators();
  }
  company?.updateValueAndValidity();
}

Step 9 — Submit handling best practices

  • Disable submit when form.invalid || form.pending
  • Call markAllAsTouched() on invalid submit
  • Log form.errors and per-control .errors for debugging
  • Use getRawValue() when disabled controls should be included
onSubmit() {
  if (this.form.invalid) {
    this.form.markAllAsTouched();
    return;
  }
  console.log(this.form.getRawValue());
}

FAQ

Reactive vs template-driven validation — which should I use?

Use reactive forms when validation logic is complex, testable, or dynamic. Template-driven forms suit simple cases with HTML5 attributes.

Why aren’t my validation errors showing?

Check that the control is touched or call markAllAsTouched(). Ensure formControlName matches the control key exactly.

How do I validate on blur instead of every keystroke?

Use updateOn: 'blur' in the FormControl config: ['', { validators: Validators.required, updateOn: 'blur' }] (Angular 14+).

Practice & related tutorials


Kindson Munonye

Kindson Munonye is a software engineer and technical author specializing in Angular, Spring Boot, and microservices architecture. He publishes step-by-step tutorials with source code covering CRUD operations, reactive forms, CQRS, event sourcing, and REST API integration.GitHub · LinkedIn · About · YouTube

View all posts by Kindson Munonye →
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted