Forms

Angular Custom Form Validators

All about custom form validators, including synchronous and asynchronous, field-level, form-level, for both template-driven and reactive forms.

Angular Custom Form Validators — Forms article by Angular University on Angular In Depth
Angular Custom Form Validators — Forms article by Angular University on Angular In Depth
On this page · 13 sections

The built-in validators that ship with Angular Forms cover the basics—required fields, email format, and length constraints—but real-world business requirements almost always demand more. When you hit that wall, you need to write your own validation logic. This guide covers everything required to build custom validators, both synchronous and asynchronous, for individual fields and entire forms, using both template-driven and reactive approaches.

Table of Contents

Here is what lies ahead in this article:

  • Defining a form field validator
  • Building custom validators for reactive forms
  • Surfacing error messages for custom validation
  • Creating custom validators for template-driven forms
  • A side-by-side comparison of validation strategies
  • Multi-field and form-level validators
  • Working with asynchronous validators
  • Leveraging the updateOn property
  • Closing thoughts

This is part of a larger series dedicated to Angular Forms. You can browse all the related pieces here.

Let’s jump straight into the mechanics of custom form validation.

What is a form field Validator?

Anyone who has worked with Angular’s form modules knows that every form tracks two key pieces of state: the value bundle, which aggregates the current values of all its fields, and a validity flag, which is true when every field passes its checks and false when any single field fails.

Individual fields carry their own set of business rules—a required entry, a minimum character count, a specific pattern—and these rules are what determine the field’s individual validity.

Consider a standard reactive login form, which might look like this:

At a glance, this form displays just an email and a password field. Because it’s reactive, there are no validation attributes to be found in the template—all the logic lives in the component class.

The component class for this form is shown here:

There is a substantial amount of validation configuration visible in that class. Let’s walk through it, starting with the straightforward parts.

This reactive form relies on the FormBuilder service to compactly declare both the fields and their validation rules.

The email field is subject to two standard rules: it must be populated (Validators.required) and it must conform to email formatting (Validators.email). These are just two samples from the built-in suite of validators that are readily available.

So how do these validators work, what is a validator?

At its core, a validator is simply a function. The form engine invokes it, passes the control as an argument, and uses the return value to determine the validity status of the field.

The function outputs true if the value complies with the rules, or false if it doesn’t. You can register a validator with a reactive form by appending it to the validators array defined for that particular control, each field having its own distinct array.

Two alternative syntaxes for configuring validators

If you look closely at the email and password fields in the example, you might notice they were configured using subtly different syntaxes.

The password field employs the familiar FormBuilder short-hand array syntax, which looks like this:

In this pattern, the array for each field lists the initial value first, followed by the array of synchronous validators.

This compact approach works well but can become unwieldy when a field needs numerous validators, making the declaration harder to parse at a glance.

The email field, by contrast, uses an expanded configuration object syntax:

This more robust—yet slightly more verbose—syntax replaces the second array slot with a configuration object that sports three optional properties:

  • validators: the array of synchronous validators applicable to this field
  • asyncValidators: the array of asynchronous validators for this field (none are used in the current example)
  • updateOn: governs the timing of when the field value and its validity are refreshed (we’ll revisit this later).

It’s worth switching to this object-based syntax the moment you need updateOn, or simply to give your form definition more breathing room and clarity.

One more thing to notice in the login form: the password field has a validator beyond the standard required and minLength checks—a function called createPasswordStrengthValidator().

That’s our very own custom validator.

Custom Validators for Reactive Forms

The strength validator pushes beyond the bare minimum character count. It might, for example, mandate that the password includes uppercase letters, digits, and symbols, not just lowercase ones.

Designing a custom validator for a reactive form is as straightforward as writing a new function:

Here’s what’s happening in that code. The createPasswordStrengthValidator() function is not itself a validator. Instead, it acts as a factory—a creation function—that returns the actual validator as its output.

The tell-tale sign is the return type of the function: ValidatorFn. You’re defining one function that manufactures another. The outer function can accept any setup parameters you like to tailor the behavior of the inner validator (this example needs none), and it yields the validator function as the final result.

That returned function is what the form engine will call, passing the control instance in, to judge whether the password is acceptable.

The validator itself has a precise contract it must honor.

How to write a Validator function

A validator that gets returned from a creation function is bound by these rules:

  • It takes a single argument: an AbstractControl. The value slated for validation is accessible via control.value.
  • If the value passes all checks, it returns null.
  • When validation fails, it produces an object shaped like —or at least compatible with—ValidationErrors.
  • The key of the returned error object typically identifies the specific problem, while its value holds the relevant details of that error.
  • The error details can be as rich as you need—a nested object is fine if you want to communicate context or specifics about the failure.
  • If you don’t need to provide details, simply setting the error’s value to true is enough to flag it.

The password strength validator we wrote checks for all the required character classes. After evaluating the value, it either:

  • yields null, signifying the password meets the standards, or
  • produces an error object like {passwordStrength:true} when the password is too weak.

That example only sets a flag, but you have full freedom to return something more descriptive, such as:

This ValidationErrors object is entirely flexible; feel free to nest information if you need to explain why the validation failed.

With our new function ready, wiring it into the form is a one-line affair:

The createPasswordStrengthValidator() function currently needs zero arguments, but if the implementation required configuration—a max length, a regex pattern—you could pass those values directly in the call, and the returned validator would use them.

Displaying error messages for custom Validators

To let the user know their password is not up to snuff, you can drop a small error message block into the template:

When our custom validator flags an issue, the returned error object is merged into the password control’s errors collection. The ngIf directive evaluates that error state, and when the validation returns false, the message renders for the user.

Custom Validators for Template-Driven Forms

Setting up custom validation in a template-driven form is comparatively involved.

In template-driven forms, the validation rules are laid out in the template via directives, rather than in the component class.

Take the login form from before—here is its template-driven equivalent:

It’s immediately obvious that the template is doing a lot more heavy lifting in this approach. You can see standard required, minlength, and email directives attached to the email and password inputs.

These directive keywords map to their respective built-in validator functions like Validators.required, Validators.minlength, and Validators.maxLength.

Note the custom passwordStrength directive applied to the password input—that one isn’t a built-in; we have to construct it ourselves.

For template-driven forms, wrapping our validator in a directive is a necessity. Without it, there is no way to attach our custom logic.

How to write a custom Validator directive

Here’s the skeleton of what our PasswordStrengthDirective looks like:

Let’s unpack this code, starting with the validation core:

  • To create a custom directive for validation, you must implement the Validator interfaced. This interface requires a single method—validate().
  • On invocation, the validate() method delegates to our validator creation function, passing the relevant control reference along.
  • The validator result is returned. It returns null when the password is valid, or a ValidationErrors object when it isn’t.

Implementing the interface is only half the story. To make this work, we must also register the directive within Angular’s dependency injection (DI) framework.

Understanding the DI configuration of a custom validator directive

The Angular Forms module discovers all available validator directives by requesting the NG_VALIDATORS injection token from the DI container.

This token is registered with multi:true, meaning the container can return multiple values for it—a necessity given that there isn’t just a single custom validator in the system.

The provider configuration inside our directive’s declaration feeds a new instance into the DI system. This setup ensures the PasswordStrengthDirective is available to be called upon when Angular instantiates the form elements that use it.

Due to the multi-value nature of this provider, we aren’t replacing the existing set of validators; we’re simply appending the PasswordStrengthDirective to the pool of available options.

This DI setup is non-negotiable. If you skip it, your custom directive will silently never run—the validate() method won’t be triggered, and the field will always pass validation.

Comparing custom Validation in template-driven vs reactive forms

Implementing custom validation in the template-driven world is demonstrably more labor-intensive.

In the reactive world, writing a single function is all it takes. In the template-driven world, you’ll need not only that function but also a custom directive, plus a working knowledge of the DI containers.

That extra ceremony is still perfectly manageable; it just requires a broader understanding of Angular’s inner workings.

For a deeper dive into the distinctions between the two approaches, have a look at our earlier piece on template-driven versus reactive forms.

Validators Spanning Multiple Form Fields

Custom validation logic isn't limited to individual form controls. We can also attach our own validators to the entire form itself.

This capability is particularly valuable for multi-field validation rules, where the validity of one field depends on the value of another. A field-level validator simply cannot access the values of its sibling fields, but a form-level validator can look at everything at once.

Consider a form that captures two dates: a start date and an end date. Simply marking both fields as required doesn't ensure that the date range is logically correct.

We need an additional rule to enforce that the start date occurs before the end date.

Crucially, this kind of cross-field check cannot be implemented with a validator scoped to a single control. It must be defined at the level of the FormGroup.

Fortunately, the process of creating a form-level validator is remarkably similar to creating a field-level one. We just write another plain function.

Here's an example of such a validator that compares two date fields:

Notice how the structure is nearly identical to a field-level validator. The one key difference is the type of the argument passed in: a form-level validator receives a FormGroup object, not a single control. With access to the group, we can pull out the values of both the start and end date controls and perform the comparison logic.

Once the validator function is defined, we just need to register it with the form's configuration:

As you can see, this validator is applied to the FormGroup itself, not to any of the individual controls within it. It sits in a separate configuration object alongside the group's other settings.

This group-level configuration object also supports the asyncValidators and updateOn properties, which we will explore in detail shortly.

Dealing with Asynchronous Validation

All the validators we've discussed so far work synchronously. The validation logic runs, and the result is available immediately when the validator function is invoked.

This approach works perfectly for checks like "is the field empty?" or "is the password long enough?" However, many real-world validation scenarios are not so immediate. We often need to perform an async operation—such as calling a backend API—to determine the validity of a field.

A synchronous validator is impractical for these cases. Let's take a typical example: a user registration form with an email field. We want to enable the form's submit button only if the email address hasn't been used already.

Checking if an email exists requires a round trip to the server and a database query. This is an asynchronous operation by nature. This is exactly the kind of problem asynchronous validators are built to solve.

Here's the template for our user creation form:

In this template, an error message is displayed if the userExists property is available in the field's errors object.

Now, let's look at the component class, focusing on the email field's setup:

Since synchronous and asynchronous validators cannot be used together on the same field, we've used the more detailed configuration object syntax to attach our async validator to the email control.

We pass a UserService instance as an argument to the userExistsValidator creation function. This service is what our validator will call to reach the backend and verify the email's uniqueness.

Signature of an Asynchronous Validator

Let's examine the implementation of our userExistsValidator:

At first glance, it looks quite similar to the synchronous validators we've already seen. The primary distinction lies in the return type. The userExistsValidator() function returns an AsyncValidatorFn.

When invoked, this validation function must return either a Promise or an Observable that emits a value of type ValidationErrors.

In this specific case, we opted for an Observable based approach. We leverage an HTTP-backed UserService to query our backend and check whether the user exists in the database.

One final, critical aspect of understanding custom validators relates to the updateOn property, which was referenced in the email field configuration above.

Controlling Validation Timing with updateOn

The updateOn property is configurable either directly on a form control (like with the email field) or on the form group as a whole (as we saw with the date range example).

This property dictates when the form model incorporates the latest value from a form field.

When a form control (e.g., an input box) is connected to the form model through directives like ngModel, formControl, or formControlName, its value becomes tracked by the parent form.

As the user types or modifies the value, those changes are propagated back up to the parent form.

Upon receiving a new value, the parent form must update its aggregate form.value payload. At the same time, it must re-evaluate the validity of that particular field, which in turn affects the form's overall validity.

This re-evaluation involves running the field's validators to compute the new validity state, and then accordingly updating the form's own status.

The question then becomes: precisely when does this value propagation and validation cycle trigger?

By default, the event stream is aggressive. For a text input, every single keystroke from the user triggers an update and validation run. This may be too frequent or resource-intensive for certain scenarios.

This is particularly troublesome for validators like our userExistsValidator. If it ran on every keystroke, we'd be bombarding the backend server with HTTP requests for each character typed.

A more sensible approach would be to wait until the user has finished typing the email and moved on, and *then* perform that single backend check. This is achieved by setting the updateOn property to blur.

With blur, the form field will only push its value to the parent form when it loses focus—for instance, when the user presses the Tab key or clicks on another area of the page. Only at that moment will a single HTTP call be made to validate the email.

It is worth noting that during the time the asynchronous validation is running, the field receives an ng-pending CSS state class. Developers can use this class to apply custom styling and indicate to the user that a check is in progress.

Why Use blur as the updateOn Strategy?

It's not just asynchronous validators that can benefit from delaying updates. The behavior of certain simpler, synchronous validators can also create a poor user experience if run too early.

Consider the built-in email validator. Triggering it with every keystroke is generally ineffective and distracting.

For instance, a user typing their email address will see a "The email is invalid" error banner appear and disappear with each keystroke, as the input momentarily fails the check before becoming a complete, valid address. This flickering can be confusing and annoying, so adopting a blur strategy here is a common UX improvement.

The Available updateOn Options

There are three distinct values you can assign to the updateOn property:

  • change: This is the default setting. The form model updates in real-time with every new field value, triggering all relevant validators. For a dropdown or checkbox, this occurs when a new selection is made. For a text input, it happens on each keypress.
  • blur: The form model only updates when the field loses focus (i.e., when the user tabs to a different control or clicks outside the field).
  • submit: This option is used less frequently but is available. The field value is only pushed to the form model after the form's submit event fires.

The updateOn property can be configured at the individual form field level, or at the form group's top-level configuration, providing flexibility for different scenarios.

Recap: Custom Form Validators

Let's go over the main takeaways from our discussion on custom form validators:

  • A custom validator is essentially a function that enforces a business rule on a field's value, such as imposing a minimum length or a required status.
  • Reactive forms offer a highly streamlined way to create and integrate custom validators. You define a straightforward function and place it into your form's configuration.
  • It’s possible to use custom validation in template-driven forms, but the process involves extra steps. Beyond the validation function, you must also define a custom directive and wire it into the dependency injection system.
  • Validators can be applied at the form level, which is essential for rules that need to compare multiple fields, like checking that two password fields match.
  • Validators that depend on asynchronous operations, like an API call, need to return a Promise or an Observable instead of a synchronous result.
  • To ensure a smooth user experience and optimal performance, it's often necessary to fine-tune the scheduling of validation events. The updateOn property is the tool to achieve this control.

I hope this deep dive has clarified how to create, configure, and optimize custom validators in Angular. If you'd like to delve even deeper into Angular Forms, please consider checking out the Angular Forms In Depth course. It covers custom form controls, multiple validators, and a host of other advanced topics in detail.

If you have any questions or feedback, feel free to use the comments section below to send me a message, and I will be sure to reply.

Don't forget to subscribe to our newsletter to stay up to date on future Angular posts.

For those of you just starting your Angular journey, make sure to take a look at our Angular for Beginners Course:

Angular Custom Form Validators — figure 1
AU
Angular University

Writes about RxJS, Components, Signals. Active 2015–2026.

All 79 articles →