← Back to Articles         
Angular Cross-Field Validation & Validation Messages

Angular Cross-Field Validation & Validation Messages

Published on 01 Oct 2026     8 min read Angular
Angular forms

Angular Cross-Field Validation & Validation Messages

1. What is Cross-Field Validation?

Cross-field validation is validation where the rule depends on the values of two or more form controls together, rather than validating one field independently.

A classic example is:

Password:         [ ******** ]
Confirm Password: [ ******** ]

        ↓

Password === Confirm Password?

Other examples include:

  • StartDate must be before EndDate
  • MinPrice must be less than MaxPrice
  • NewPassword must differ from OldPassword
  • FromAmount must be less than ToAmount

Because the rule involves multiple controls, the validator is usually attached to their parent FormGroup.


2. Full Simple Sample Program

We'll create a registration form with:

  • Password
  • Confirm Password
  • Required validation
  • Minimum-length validation
  • Cross-field password matching
  • Validation messages

app.ts

import { Component } from '@angular/core';
import {
  AbstractControl,
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  ValidationErrors,
  ValidatorFn,
  Validators
} from '@angular/forms';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './app.html'
})
export class App {

  userForm = new FormGroup(
    {
      password: new FormControl('', [
        Validators.required,
        Validators.minLength(6)
      ]),

      confirmPassword: new FormControl('', [
        Validators.required
      ])
    },
    {
      validators: [this.passwordMatchValidator()]
    }
  );

  passwordMatchValidator(): ValidatorFn {

    return (group: AbstractControl): ValidationErrors | null => {

      const password = group.get('password')?.value;
      const confirmPassword = group.get('confirmPassword')?.value;

      if (password !== confirmPassword) {
        return {
          passwordMismatch: true
        };
      }

      return null;
    };
  }

  submitForm() {

    if (this.userForm.valid) {

      console.log('Form Submitted');
      console.log(this.userForm.value);

    } else {

      this.userForm.markAllAsTouched();

    }
  }
}

app.html

<h2>Registration Form</h2>

<form
  [formGroup]="userForm"
  (ngSubmit)="submitForm()">

  <!-- Password -->

  <div>

    <label>Password:</label>

    <input
      type="password"
      formControlName="password">

    @if (userForm.controls.password.touched) {

      @if (userForm.controls.password.hasError('required')) {
        <p>Password is required.</p>
      }

      @if (userForm.controls.password.hasError('minlength')) {
        <p>Password must contain at least 6 characters.</p>
      }

    }

  </div>

  <br>

  <!-- Confirm Password -->

  <div>

    <label>Confirm Password:</label>

    <input
      type="password"
      formControlName="confirmPassword">

    @if (userForm.controls.confirmPassword.touched) {

      @if (userForm.controls.confirmPassword.hasError('required')) {
        <p>Confirm Password is required.</p>
      }

      @if (
        userForm.hasError('passwordMismatch') &&
        !userForm.controls.confirmPassword.hasError('required')
      ) {
        <p>Password and Confirm Password do not match.</p>
      }

    }

  </div>

  <br>

  <button type="submit">
    Submit
  </button>

</form>

3. How Cross-Field Validation Works

Normally, a validator is attached to one control.

For example:

password: new FormControl('', [
  Validators.required
])

That validator only needs to inspect:

password

But password matching requires:

password
      +
confirmPassword
      ↓
Compare both values

Therefore, we attach the validator to the FormGroup:

new FormGroup(
  {
    password: ...,
    confirmPassword: ...
  },
  {
    validators: [this.passwordMatchValidator()]
  }
);

Conceptually:

userForm (FormGroup)
│
├── password
│      └── required
│      └── minlength
│
├── confirmPassword
│      └── required
│
└── passwordMatchValidator
       │
       ├── reads password
       ├── reads confirmPassword
       │
       └── compares them

4. Creating the Cross-Field Validator

Our validator is:

passwordMatchValidator(): ValidatorFn {

  return (group: AbstractControl): ValidationErrors | null => {

    const password = group.get('password')?.value;
    const confirmPassword = group.get('confirmPassword')?.value;

    if (password !== confirmPassword) {

      return {
        passwordMismatch: true
      };

    }

    return null;
  };
}

First, get the password:

const password = group.get('password')?.value;

Then get the confirmation:

const confirmPassword =
  group.get('confirmPassword')?.value;

Then compare:

if (password !== confirmPassword)

If they don't match:

return {
  passwordMismatch: true
};

If they match:

return null;

Remember:

ValidationErrors object
        ↓
      INVALID


null
 ↓
VALID

5. Where Is the Error Stored?

This is an important point.

Because our validator is attached to:

userForm

the error belongs to the FormGroup.

Therefore, check:

userForm.hasError('passwordMismatch')

not:

userForm.controls.confirmPassword.hasError('passwordMismatch')

Conceptually:

userForm
│
├── password
│    └── required/minlength errors
│
├── confirmPassword
│    └── required error
│
└── passwordMismatch
     ↑
     FormGroup-level error

6. Cross-Field Validation Example

Suppose the user enters:

Password:         Angular123
Confirm Password: Angular456

The validator compares:

Angular123 !== Angular456

Therefore it returns:

{
  passwordMismatch: true
}

The form becomes:

INVALID

If the user changes it to:

Password:         Angular123
Confirm Password: Angular123

the validator returns:

null

and the cross-field rule passes.


7. What Other Cross-Field Validations Are Common?

Start Date vs End Date

Start Date: 10-Oct-2026
End Date:   05-Oct-2026

             ↓

          INVALID

Rule:

StartDate <= EndDate

Minimum vs Maximum Price

Minimum Price: 5000
Maximum Price: 3000

         ↓

      INVALID

Rule:

Minimum Price <= Maximum Price

These validators belong naturally at the group level because they need access to multiple controls.


8. How Do You Display Validation Messages?

Angular doesn't automatically decide how your validation errors should appear.

We check the control's validation state and display the appropriate message.

A common pattern is:

@if (control.invalid && control.touched) {
  ...
}

For example:

@if (
  userForm.controls.password.invalid &&
  userForm.controls.password.touched
) {
  <p>Password is invalid.</p>
}

9. Display Different Messages for Different Errors

Suppose:

password: new FormControl('', [
  Validators.required,
  Validators.minLength(6)
])

There are two possible validation errors.

We shouldn't show the same message for both.

Instead:

@if (userForm.controls.password.touched) {

  @if (userForm.controls.password.hasError('required')) {
    <p>Password is required.</p>
  }

  @if (userForm.controls.password.hasError('minlength')) {
    <p>Password must contain at least 6 characters.</p>
  }

}

This gives the user a precise explanation.


10. What is hasError()?

hasError() checks whether a particular validation error currently exists.

For example:

userForm.controls.password.hasError('required')

checks:

Does password have a
"required" validation error?

Similarly:

userForm.controls.password.hasError('minlength')

checks the minimum-length error.

And:

userForm.hasError('passwordMismatch')

checks our custom FormGroup-level error.


11. Why Check touched?

Consider:

@if (
  userForm.controls.password.hasError('required')
) {
  <p>Password is required.</p>
}

As soon as the page loads:

Password = ""
      ↓
required fails
      ↓
Error displayed immediately

Usually we don't want that.

Instead:

@if (
  userForm.controls.password.touched &&
  userForm.controls.password.hasError('required')
) {
  <p>Password is required.</p>
}

Now:

Page loads
    ↓
No error displayed

User enters Password field
    ↓
User leaves field
    ↓
touched = true
    ↓
If invalid
    ↓
Display error

12. touched vs dirty

Both are frequently used when deciding when to display validation messages.

touched

The user entered the control and then left it.

Focus field
    ↓
Leave field
    ↓
touched = true

dirty

The user changed the value.

Original value
    ↓
User modifies it
    ↓
dirty = true

A common condition is:

control.invalid &&
(control.touched || control.dirty)

Example:

@if (
  userForm.controls.password.invalid &&
  (
    userForm.controls.password.touched ||
    userForm.controls.password.dirty
  )
) {
  <p>Password is invalid.</p>
}

13. Displaying Messages After Submit

There is another problem.

Suppose the user opens the form and immediately clicks:

Submit

The controls may still be:

touched = false

So the error messages might not display if your template depends on touched.

That's why our submit method uses:

this.userForm.markAllAsTouched();

Example:

submitForm() {

  if (this.userForm.valid) {

    console.log(this.userForm.value);

  } else {

    this.userForm.markAllAsTouched();

  }
}

Now:

User clicks Submit
       ↓
Form invalid
       ↓
markAllAsTouched()
       ↓
Controls become touched
       ↓
Validation messages displayed

This is a common and useful pattern.


14. Field-Level vs Cross-Field Validation Messages

It's important to distinguish them.

Field-Level Error

Example:

Validators.required

Check the individual control:

userForm.controls.password.hasError('required')

Cross-Field Error

Example:

passwordMismatch

Check the group:

userForm.hasError('passwordMismatch')

So:

Individual validation
        ↓
FormControl.errors


Cross-field validation
        ↓
FormGroup.errors

15. Complete Validation Flow

User enters Password
        ↓
FormControl validators
        ↓
required / minlength
        ↓
User enters Confirm Password
        ↓
FormControl validator
        ↓
required
        ↓
FormGroup validator executes
        ↓
Compare:
Password vs Confirm Password
        ↓
   ┌───────────────┐
   │               │
 Match          No Match
   │               │
   ↓               ↓
 null        passwordMismatch
   │               │
   ↓               ↓
 VALID           INVALID

The template then reads those states and displays the appropriate messages.


16. Key Points

  • Cross-field validation validates multiple related fields together.
  • It is commonly implemented with a custom validator attached to a FormGroup.
  • Password/Confirm Password is a classic example.
  • Other examples include Start/End Date and Min/Max values.
  • A valid custom validator returns null.
  • An invalid validator returns a ValidationErrors object.
  • A group-level validator's error normally exists on the FormGroup.
  • Use hasError() to check specific validation errors.
  • Use touched or dirty to control when validation messages appear.
  • markAllAsTouched() is useful when the user submits an invalid form.
  • Show specific messages for specific validation failures.

17. Interview Questions and Answers

1. What is cross-field validation?

Cross-field validation is validation where the rule depends on multiple form controls.

For example:

Password === ConfirmPassword

It is commonly implemented using a custom validator attached to a FormGroup.


2. Why attach a cross-field validator to FormGroup?

Because the validator needs access to multiple controls within the group.

const password = group.get('password')?.value;

const confirmPassword =
  group.get('confirmPassword')?.value;

3. Give examples of cross-field validation.

Common examples include:

  • Password and Confirm Password match
  • Start Date is before End Date
  • Minimum value doesn't exceed Maximum value
  • New Password differs from Old Password

4. What should a custom validator return?

Valid
  ↓
null

Invalid
  ↓
ValidationErrors object

Example:

return {
  passwordMismatch: true
};

5. How do you display validation messages?

Check the control's state and specific validation error.

@if (
  userForm.controls.password.touched &&
  userForm.controls.password.hasError('required')
) {
  <p>Password is required.</p>
}

6. What is hasError()?

hasError() checks whether a control or group currently contains a specific validation error.

control.hasError('required')

7. Where is a cross-field validation error stored?

If the validator is attached to the FormGroup, its error is normally stored on that FormGroup.

userForm.hasError('passwordMismatch')

8. Why use touched while displaying validation messages?

It prevents errors from being displayed before the user has interacted with the field.


9. What does markAllAsTouched() do?

It marks all descendant controls as touched.

this.userForm.markAllAsTouched();

It is commonly used after an invalid form submission so the validation messages become visible.


Interview Summary

Cross-field validation validates related controls together, such as Password and Confirm Password, and is typically implemented with a custom validator attached to a FormGroup. Validation messages are displayed by checking control/group state such as touched or dirty together with specific errors using hasError().

← Back to Articles