Signal forms: compatForm() FormControl errors are missing from errorSummary()
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- angular, typescript
- Domain
- frontend
Research direction
Read packages/forms/signals/compat/src/compat_validation_state.ts and packages/forms/signals/src/compat/validation_errors.ts, then compare CompatValidationState with FieldValidationState and the addDefaultField() helper in packages/forms/signals/src/field/validation.ts. Run the referenced compat_error_summary.spec.ts cases with pnpm bazel test //packages/forms/signals/test/node:test --test_output=all. Done means compat control errors appear in errorSummary() with a usable fieldTree, and the existing summary ordering works.
Written by the indexing model from the issue text.
Description
Which @angular/* package(s) are the source of the bug?
forms
Is this a regression?
No
Description
In short. A form built with compatForm() can be invalid because of a FormControl validator,
while its errorSummary() is empty. The errors also have no fieldTree, although their type says
they do.
Why it matters. errorSummary() is how an app lists every error at once, for example next to
the submit button. With compat fields, submit() is blocked but the list is empty, so the user
can't tell what to fix. Code that follows an error to its field, such as
error.fieldTree().focusBoundControl(), throws on a compat error.
Failure mode in user code.
@Component({
imports: [FormField],
template: `
<input [formField]="f.name" />
<button (click)="save()">Save</button>
@if (showErrors()) {
<ul>
@for (error of f().errorSummary(); track error) {
<li>{{ error.kind }}</li>
}
</ul>
}
`,
})
export class Signup {
readonly showErrors = signal(false);
readonly model = signal({
name: new FormControl('', {nonNullable: true, validators: Validators.required}),
});
readonly f = compatForm(this.model);
save() {
submit(this.f, {
action: async () => saveSignup(this.model().name.value),
onInvalid: () => this.showErrors.set(true),
});
}
}
Starting state: the FormControl value is ''. f.name().errors() holds one required error,
and f().invalid() is true.
Current behavior:
1. User clicks Save the action doesn't run; onInvalid shows the list
f().errorSummary() → []
the list is empty
Expected behavior:
1. User clicks Save the action doesn't run; onInvalid shows the list
f().errorSummary() → [{kind: 'required', fieldTree: f.name}]
the list shows "required"
Assumptions: expected vs current.
| Expected | Current |
|---|---|
errorSummary() holds "the errors of the field and its descendants", as its doc comment says. |
A compat field's errorSummary is a shared empty signal. |
invalid() and errorSummary() agree: an invalid form has at least one error in its summary. |
f().invalid() is true while f().errorSummary() is []. |
Every error typed ValidationError.WithFieldTree has a fieldTree that points at its field, as plain errors do. |
CompatValidationError declares fieldTree but never sets it. |
Affected areas.
CompatValidationStateinpackages/forms/signals/compat/src/compat_validation_state.ts:
errorSummaryanderrors.CompatValidationErrorinpackages/forms/signals/src/compat/validation_errors.ts.errorSummary()on any form with compat fields, including its DOM-order sort.- Consumers that read
error.fieldTreeon compat errors.
Tradeoffs of the fix.
- The summary fix needs
fieldTreefirst. On the client, the root summary sorts errors by DOM
position througherror.fieldTree(), so adding compat errors without it would throw a
TypeErroronce the summary has two or more errors. - Setting
fieldTreeinCompatValidationStatewith the existingaddDefaultField()helper keeps
the publicCompatValidationErrorclass unchanged. Adding a constructor parameter instead
changes public API. - Nested errors of a
FormGrouporFormArrayvalue all point at the compat field, because
nested controls have noFieldTreeof their own. - A compat field bound with
[formControl]instead of[formField]has no bound element, so its
errors sort to the end of the summary.
Related. #71633, where compat fields drop parse errors, changes the same class.
Details.
- Triage PR with the recommended fix, design decisions, and questions: #71630
- Triage notes:
observed output, root cause, and the sort hazard. CompatValidationState.errorSummary,
FieldValidationState.errorSummary,
CompatValidationError,
addDefaultField(),
DOM-order sort.
Please provide a link to a minimal reproduction of the bug
compat_error_summary.spec.ts
in the triage PR. Each spec asserts the expected behavior, so 2 of them fail today:
pnpm bazel test //packages/forms/signals/test/node:test --test_output=all
Please provide the exception or error you saw
No error is thrown. The repro specs report:
root errorSummary(): Expected $.length = 0 to equal 1.
fieldTree of the compat field error: Expected undefined to be Function.
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/node:test.
The affected source files are unchanged on main at 3e173fc844 (2026-10-24).
Anything else?
I searched for an existing issue or PR and found none.
- Dominant language
- TypeScript
- Stars
- 102k
- Forks
- 28.2k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 417
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from angular/angular
-
Two-way binding documentation is missing important informationMay be free again A pull request for this issue was closed without being merged. Openarea: docs
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
angular/angular#71796 · 2 comments · 1 reaction ·
Maintainers usually reply within 1 day
-
area: core
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
angular/angular#71585 · 3 comments ·
Maintainers usually reply within 1 day
-
Assigning to a template reference in an event binding isn't reported by the compilerMay be free again A pull request for this issue was closed without being merged. Openarea: compiler
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
angular/angular#71511 · 3 comments ·
Maintainers usually reply within 1 day
-
Signal Forms: `validateStandardSchema` throws a TypeError for an issue whose path runs through a key the model lacksPossibly taken A pull request linked to this issue is open or already merged. Openarea: forms gemini-triaged
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
`Symbol.iterator in FieldTree` is always falseMay be free again A pull request for this issue was closed without being merged. Openarea: forms forms: signals
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
angular/angular#70252 · 3 comments ·
Maintainers usually reply within 1 day
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
jeanibarz/knowledge-base-mcp-server#1268 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 78/100
Koenkk/zigbee-herdsman-converters#13820 ·
Maintainers usually reply within 2 days
-
[prefer-expect-resolves] autofix leaves a stray ")" when the awaited expression is parenthesizedOpen
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
jest-community/eslint-plugin-jest#2052 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Wallet functions throws TypeError when the wallet has no account IDPossibly taken @Medhansh-741 claimed this today. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
hiero-ledger/hiero-sdk-js#4781 · 1 comment ·
Maintainers usually reply within 1 day