When built-in validators don't cover a domain-specific rule, you can write your own. This lesson covers the validator function signature and how to build reusable, parameterized validators.
What Is a Custom Validator?
A custom validator is a function matching the ValidatorFn signature: it receives an AbstractControl and returns either null (valid) or a ValidationErrors object describing what failed, exactly the same contract Angular's own built-in validators follow.
Writing validators as plain functions makes them easy to reuse across different forms and easy to unit test in isolation, without rendering any component.
A validator receives the control and returns null if valid, or an object describing the failure if invalid.
The error object's key (like myError) becomes the name you check with control.hasError('myError').
Parameterized validators are usually written as a "factory" function returning a ValidatorFn.
Async validators follow the same idea but return an Observable/Promise instead of a synchronous value.
Custom Validators Cheatsheet
Patterns for building your own validators.
Pattern
Example
Use Case
Simple validator function
function noSpaces(control) { ... }
One fixed rule, no parameters
Validator factory
function minAge(min: number): ValidatorFn
Parameterized, reusable rule
Cross-field validator
Attached via FormGroup's options
Rule depends on multiple controls
Async validator factory
function uniqueEmail(api): AsyncValidatorFn
Rule needs a server round-trip
Checking a specific error
control.hasError('minAge')
Show a targeted error message
A Validator Factory With Parameters
When a validation rule needs a configurable threshold, wrap it in a factory function that accepts the parameter and returns the actual ValidatorFn, closing over that parameter.
export function minAgeValidator(minAge: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const age = Number(control.value);
return age >= minAge ? null : { minAge: { required: minAge, actual: age } };
};
}
// usage
age = new FormControl(null, [minAgeValidator(18)]);
Returning extra detail (required, actual) in the error object lets the template build a precise message like "Must be at least 18 (you entered 15)."
A Reusable Cross-Field Validator
Cross-field validators can also be written as reusable factories, taking the names of the fields to compare, rather than hardcoding field names inside the function.
export function matchFieldsValidator(fieldA: string, fieldB: string): ValidatorFn {
return (group: AbstractControl): ValidationErrors | null => {
const a = group.get(fieldA)?.value;
const b = group.get(fieldB)?.value;
return a === b ? null : { fieldsMismatch: true };
};
}
form = this.fb.group(
{ password: [''], confirmPassword: [''] },
{ validators: matchFieldsValidator('password', 'confirmPassword') }
);
Unit Testing a Custom Validator
Because validators are plain functions, they can be tested directly with a FormControl instance, no component rendering or TestBed setup required.
it('rejects an age below the minimum', () => {
const control = new FormControl(15);
const result = minAgeValidator(18)(control);
expect(result).toEqual({ minAge: { required: 18, actual: 15 } });
});
Common Mistakes
Writing a validator directly as an inline arrow function instead of a reusable, testable named function.
Forgetting that a validator factory (like minAgeValidator(18)) must be *called* to produce the actual ValidatorFn.
Attaching a cross-field validator to a single control instead of the parent FormGroup.
Returning false/true from a validator instead of null/an error object, breaking Angular's expected contract.
Key Takeaways
A custom validator is a function matching ValidatorFn, returning null or an errors object.
Validator factories let you build parameterized, reusable validation rules.
Cross-field validators are attached at the FormGroup level and compare multiple controls.
Because validators are plain functions, they're simple to unit test in isolation.
Pro Tip
Include contextual detail in your custom error objects (like the required vs actual value) rather than just true; it lets your templates build specific, helpful messages instead of a generic "invalid" notice.
You now know how to build custom validators. Next, move into Angular's HttpClient to learn how to communicate with backend APIs.