Which @angular/* package(s) are the source of the bug?
forms
Is this a regression?
No
Description
In short. When a compatForm() field is bound with [formField] to a control that can't parse
what the user typed, the field ignores the parse error. For example, <input type="number"> with
30e in it. The field stays valid, no error shows, and submit() runs with the last value that
parsed. The same template on a plain form() field blocks the submit.
Why it matters. compatForm() is the documented way to migrate a reactive form to signal forms
step by step, and the migration guide
binds compat fields with [formField]. The failure is silent: the user sees one value, and the app
saves another.
Failure mode in user code.
@Component({
imports: [FormField],
template: `
<input type="number" [formField]="f.age" />
@if (f.age().invalid()) {
<p>Enter a valid age.</p>
}
<button (click)="save()">Save</button>
`,
})
export class Profile {
readonly model = signal({age: new FormControl(30, {nonNullable: true})});
readonly f = compatForm(this.model);
save() {
submit(this.f, async () => {
await saveProfile({age: this.model().age.value});
});
}
}
Starting state: the FormControl value is 30, the input shows 30, f.age().errors() is [],
and f.age().invalid() is false.
Current behavior:
1. User types "30e" input shows "30e"; FormControl value stays 30
f.age().errors() → []
f.age().invalid() → false
no error message
2. User clicks Save the action runs and saves {age: 30}
Expected behavior:
1. User types "30e" input shows "30e"; FormControl value stays 30
f.age().errors() → [{kind: 'parse', ...}]
f.age().invalid() → true
"Enter a valid age." shows
2. User clicks Save the action doesn't run
Assumptions: expected vs current.
| Expected |
Current |
A compat field reports its controls' parse errors like a plain field does: in errors(), in invalid(), and at the submit gate. |
CompatValidationState.parseErrors is hardcoded to []. |
The FormControl's own state still shows through ("Reactive state is proxied automatically" in the guide). Parse errors add to it. |
errors, valid, invalid, and syncValid come only from the FormControl. |
| The control that reported the parse error can show it. |
FormField.errors() filters the field's errors(), so the binding's own errors are empty too. |
Affected areas.
CompatValidationState in packages/forms/signals/compat/src/compat_validation_state.ts:
parseErrors, errors, syncValid, valid, and invalid.
- Every source of parse errors, on a compat field:
- Native inputs with bad input, such as number, date, and time.
- Legacy
NG_VALIDATORS on a CVA, which are shimmed into parse errors.
- Custom controls that report
parseErrors.
submit() on a compat root, and on a plain root with compat children.
- The migration guide's statements that reactive state is proxied and that
errors() returns
CompatValidationError.
Tradeoffs of the fix.
- After the fix,
f.age().invalid() is true while the FormControl and its parent FormGroup
still report valid. Parse errors live in the signal-forms layer, so reactive code that checks
formGroup.valid disagrees with submit().
- Writing parse errors into the
FormControl with setErrors() would avoid that split, but the next
updateValueAndValidity() wipes them, and it fights reactive forms' ownership of the control.
errors() would mix CompatValidationError with parse errors, which have no control or
context. The public type doesn't change, and the guide's errorMessage() helper on main
already falls back to error.message ?? error.kind.
- Only documenting the limitation is cheapest, but it leaves a silent submit with the wrong value.
For comparison, the two APIs that compat bridges both catch this case. Reactive forms'
NumberValueAccessor writes null for bad input, so Validators.required fails. Plain signal
forms block the submit.
Related bug, same file. A compat field's errorSummary is always empty, so the root's
errorSummary() omits every FormControl error: #71634.
Details.
Please provide a link to a minimal reproduction of the bug
compat_parse_errors.spec.ts
in the triage PR. The repro spec asserts the expected behavior and fails today. The web test
runner ignores --filter, so this runs the whole Chromium suite:
pnpm bazel test //packages/forms/signals/test/web:test_chromium --test_output=all
Please provide the exception or error you saw
No error is thrown. The repro spec reports:
Expected $.fieldErrors.length = 0 to equal 1.
Expected $.fieldInvalid = false to equal true.
Expected $.rootInvalid = false to equal true.
Unexpected $.actionSaw[0] = 5 in array.
Please provide the environment you discovered this bug in (run ng version)
angular/angular main at ce30bb0889 (2026-10-16), Bazel target
//packages/forms/signals/test/web:test_chromium. The affected source files are unchanged on main at
3e173fc844 (2026-10-24). Native inputs have produced parse errors since v22.0.0-next.0.
Anything else?
I searched for an existing issue or PR and found none.