Eight Angular validators that quietly do nothing
The short answer
<input> values, which are always strings. Give them anything else — a number, a boolean, a null — and some of them coerce, some of them silently return null as though everything is fine, and none of them warn you. Every case below passes when you would swear it should fail, and all eight were reproduced against Angular 22.1.0.
null, the control goes green, the form submits, and the bad value lands in your database.
1. Validators.pattern rewrites your regex — unless you pass a RegExp
string and Angular wraps it in ^ and $ for you. Pass the identical pattern as a RegExp and it does not. Same characters, opposite result.
new FormControl('abc123', Validators.pattern('[a-z]+')); // ?
new FormControl('abc123', Validators.pattern(/[a-z]+/)); // ?pattern('[a-z]+') on 'abc123' -> invalid
pattern(/[a-z]+/) on 'abc123' -> VALID
errors: { pattern: { requiredPattern: "^[a-z]+$", actualValue: "abc123" } }requiredPattern is "^[a-z]+$", a regex you never wrote. The RegExp form is left alone, so /[a-z]+/ matches the abc at the front of abc123 and reports success.
2. An unchecked checkbox passes required
Validators.required asks whether the value is empty, and false is not empty — it is a perfectly good boolean.
required(false) <- unchecked box -> VALID
required(0) -> VALID
required(' ') <- a single space -> VALID
required('') -> invalid
required(null) -> invalid
required([]) -> invalidValidators.requiredTrue for the checkbox — it exists for exactly this. The single space is the sneakier one: ' ' has a length, so a "required" name field is satisfied by the space bar. If that matters, trim on input rather than hoping the validator will.
3. Validators.email is happy with a@b
email('a@b') -> VALID
email('test@localhost') -> VALID
email('') -> VALID
email('a@b.') -> invalida@b genuinely is a deliverable address on an intranet, and the validator follows the WHATWG definition rather than anyone’s intuition. The empty string passing is the more common trip-up: email says nothing about presence, so a field that must be filled in needs [Validators.required, Validators.email]. And no regex settles whether an address exists — only sending to it does.
4. minLength does nothing to a number
minLength and maxLength read value.length. A number does not have one, so they return null and move on. Meanwhile min and max do coerce — they will happily parse a string for you.
minLength(3) on the number 12345 -> VALID (no-op)
maxLength(2) on the number 12345 -> VALID (no-op)
max(10) on the string '999' -> invalid (coerced)<input type="number"> where someone reached for minLength(4) to mean "at least four digits" — that check has never run.
5. Disabled controls disappear from form.value
const form = fb.group({
name: ['Ada'],
plan: [{ value: 'pro', disabled: true }],
});form.value -> { name: 'Ada' }
form.getRawValue() -> { name: 'Ada', plan: 'pro' }required field that happens to be disabled will not stop the form being valid. Both behaviours are intentional — a disabled field is "not part of this submission" — and both are a surprise when the field was disabled for a purely visual reason. If you want the value, ask for it with getRawValue().
6. A form can be neither valid nor invalid
DISABLED. Both valid and invalid compare against a status that is now neither.
form.status -> 'DISABLED'
form.valid -> false
form.invalid -> false[disabled]="form.invalid" leaves your submit button enabled on a form nobody can fill in — and the equally reasonable [disabled]="!form.valid" disables it forever on a form that is merely read-only. The same trap exists for PENDING while an async validator is in flight: not valid, not invalid, just not finished. Test the status you actually mean.
7. { validator: fn } runs nothing
AbstractControl takes validators, plural. The singular spelling is not a key it knows, so it is ignored the way any unknown property would be.
new FormGroup(controls, { validator: fn }) -> errors: null valid: true
new FormGroup(controls, { validators: fn }) -> errors: { mismatch: true } valid: falseAbstractControlOptions is satisfied by an object with none of its optional keys. The cruel part is that FormBuilder.group() still honours the singular form through a legacy path, so the identical typo works in one file and silently disables validation in the next.
8. setErrors() in a group validator deletes the errors underneath it
setErrors(). That method replaces the control’s entire error object rather than merging into it.
confirm.errors -> null
confirm.valid -> true <- five characters, minLength(8)
form.valid -> false <- and no message anywhere near the causeelse branch calls setErrors(null) and wipes the minlength error Angular set moments earlier. The form still refuses to submit, because the other field kept its own error — so the user gets a dead button and a clean-looking form. The fix, and the full reproduction, are in the cross-field guide.
The pattern behind all eight
"". Reactive forms then let you put anything at all in a control — booleans, numbers, objects, null — and the validators kept their original assumptions. Where the assumption fails they return null, because a validator that threw on an unexpected type would break more applications than it saved.
null means "I have nothing to say", not "this value is good". Those are the same thing right up until the moment they are not, and the cheapest defence is to write one test per validator that asserts the value you expect to be rejected actually is. Four lines, and it catches every item on this list.
it('rejects an unchecked box', () => {
const control = new FormControl(false, Validators.requiredTrue);
expect(control.valid).toBe(false);
});Questions
- Why does Validators.required pass on false and 0?
- It tests whether the value is empty, not whether it is truthy. Angular treats null, undefined, the empty string and an empty array as empty; false and 0 are ordinary values. Use Validators.requiredTrue for a checkbox that must be ticked.
- Does Validators.pattern anchor my regular expression?
- Only when you pass it as a string. Angular wraps a string pattern in ^ and $ before compiling it. A RegExp is used exactly as given, so it matches anywhere in the value unless you anchor it yourself.
- Why is my Angular form neither valid nor invalid?
- Its status is DISABLED or PENDING. Both valid and invalid compare against a status of VALID or INVALID respectively, so both return false while a form is entirely disabled or while an async validator has not resolved.
- Why is a form field missing from form.value?
- It is disabled. Disabled controls are omitted from value entirely and are exempt from validation. Use form.getRawValue() to get every control including the disabled ones.