Skip to content
Angular ng forms 4 min read

Built-in Validators

Most form validation needs — a required field, a minimum length, a numeric range, a well-formed email address — are covered out of the box by Angular’s Validators class. These are pure, synchronous functions you attach to a form control; Angular runs them on every value change and reflects the result in the control’s errors, valid, and invalid state. Knowing the built-in set well means you write less custom code and get consistent, predictable validation across reactive and template-driven forms.

The Validators class

In reactive forms you pass validator functions as the second argument to a FormControl (a single function or an array). Each validator returns either null when the value is valid, or a ValidationErrors object (a key/value map) describing what failed. Angular merges those objects so a control can carry several errors at once.

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, 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({
    name: ['', [Validators.required, Validators.minLength(2)]],
    age: [null, [Validators.required, Validators.min(18), Validators.max(120)]],
    email: ['', [Validators.required, Validators.email]],
    username: ['', [Validators.required, Validators.pattern(/^[a-z0-9_]+$/)]],
  });
}

Built-in validator reference

ValidatorUse it forError keyError payload
Validators.requiredA value must be present (non-empty, non-null, non-false)requiredtrue
Validators.requiredTrueA checkbox must be ticked (e.g. accept terms)requiredtrue
Validators.min(n)Numeric value must be >= nmin{ min, actual }
Validators.max(n)Numeric value must be <= nmax{ max, actual }
Validators.minLength(n)String/array length must be >= nminlength{ requiredLength, actualLength }
Validators.maxLength(n)String/array length must be <= nmaxlength{ requiredLength, actualLength }
Validators.emailValue must look like an email addressemailtrue
Validators.pattern(regex)Value must match a regular expressionpattern{ requiredPattern, actualValue }
Validators.nullValidatorA no-op (always valid); useful as a placeholder

The error payload matters: because minlength gives you requiredLength and actualLength, you can build messages like “Needs 8 characters, you have 5” without hard-coding numbers in the template.

Reading validation state in the template

With the new control flow, render messages by checking specific error keys. Guard on touched (or dirty) so users don’t see errors before they’ve interacted with a field.

<form [formGroup]="form">
  <input formControlName="name" placeholder="Name" />
  @if (form.controls.name.touched && form.controls.name.invalid) {
    @if (form.controls.name.hasError('required')) {
      <small class="error">Name is required.</small>
    }
    @if (form.controls.name.hasError('minlength')) {
      <small class="error">
        At least
        {{ form.controls.name.getError('minlength').requiredLength }}
        characters.
      </small>
    }
  }

  <input formControlName="age" type="number" placeholder="Age" />
  @if (form.controls.age.hasError('min')) {
    <small class="error">You must be 18 or older.</small>
  }
</form>

Composing validators

Validators.compose([...]) merges several validators into one function, which is handy when you store reusable validator bundles. Passing an array directly to a control does the same thing, so reach for compose only when you need a single callable.

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

export const strongName: ValidatorFn = Validators.compose([
  Validators.required,
  Validators.minLength(2),
  Validators.maxLength(50),
])!;

Template-driven forms

In template-driven forms you don’t call Validators directly — you use HTML5-style attribute directives, and Angular wires them to the same underlying validator functions. The error keys produced are identical, so your message logic carries over.

<form #f="ngForm">
  <input
    name="email"
    ngModel
    required
    email
    #email="ngModel"
  />
  @if (email.touched && email.invalid) {
    @if (email.hasError('required')) {
      <small class="error">Email is required.</small>
    }
    @if (email.hasError('email')) {
      <small class="error">Enter a valid email address.</small>
    }
  }
</form>

Available attribute directives mirror the class: required, minlength, maxlength, min, max, email, and pattern. Bind dynamic limits with property syntax, e.g. [minlength]="minChars".

Inspecting errors at runtime

Logging a control’s errors object is the fastest way to see exactly what a validator reports.

const ctrl = this.form.controls.age;
ctrl.setValue(12);
console.log(ctrl.errors);

Output:

{ min: { min: 18, actual: 12 } }

Best practices

  • Order validators from cheapest to most specific (required first) — Angular runs all of them, but reading them in that order keeps intent clear.
  • Prefer the error payload (requiredLength, min, max) over magic numbers so messages stay in sync with the rules.
  • Gate error display on touched or dirty to avoid flagging untouched fields on first render.
  • Use Validators.requiredTrue for “accept terms” checkboxes — plain required passes when the box is unchecked-but-present in some setups.
  • Treat Validators.email and pattern as a first line of defence only; always re-validate on the server.
  • Keep template-driven and reactive validation consistent by relying on the same error keys for shared message components.
Last updated June 14, 2026
Was this helpful?