Which @angular/* package(s) are the source of the bug?
compiler-cli, forms
Is this a regression?
No
Description
The NG8115 extended diagnostic (uninvokedTrackFunction) fires on the Signal Forms field-identity tracking that the docs recommend, whenever the FieldTree is reached through a property read instead of directly as the @for loop variable.
Field state management → Tracking values for array fields says:
In signal forms, a @for block over a set of fields should be tracked by field identity.
@for (field of form.emails; track field) {
<input [formField]="field" />
}
The forms system is already tracking the model values within the array and maintaining a stable identity of the fields it creates automatically.
That exact form is fine. But it is common to iterate a small view model that pairs each field with something else (an index for a remove button, a sibling value from the model, drag state), and then the very same field identity is expressed as row.field:
@for (row of rows(); track row.field) { ... } <!-- NG8115 -->
Both loops track the identical object with the identical semantics; only the shape of the expression differs. The first is silent, the second warns.
Cause
UninvokedTrackFunctionCheck warns when the track expression is a PropertyRead/SafePropertyRead whose type has at least one call signature. FieldTree<T> is () => FieldState<T> plus proxied children, so it is callable — but the call signature is the state accessor, and its identity is precisely what the docs ask you to track. The check cannot distinguish "identity of a callable object" from "forgot to invoke the track function".
The bare-loop-variable case escapes only by accident: SymbolBuilder.getSymbolOfTemplateExpression resolves it via getExpressionTarget to a SymbolKind.Variable, and the check requires SymbolKind.Expression. So the exemption is a symbol-kind artifact, not a carve-out for Signal Forms — which is why the documented shape passes and every other spelling of the same intent does not.
The suggested fix in the message is actively harmful here: invoking with arguments is meaningless for a FieldTree, and the two obvious ways to make the warning go away — tracking by $index, or by a value such as row.field().value() — are exactly the patterns Signal Forms warns against (see #66711, #66796). Tracking by index reuses stateful inputs across a delete and moves focus to the wrong row.
Suggested fix
Skip the check when the track expression's type is a FieldTree (or, more generally, when the type is callable with zero required parameters and is not a template reference to a method), so that field-identity tracking is accepted regardless of how the field is reached.
Please provide a link to a minimal reproduction of the bug
https://stackblitz.com/edit/stackblitz-starters-hedc1b4t?file=src%2Fmain.ts
The whole repro is the single component in src/main.ts. It produces exactly one NG8115, pointing at loop B; loop A (the documented shape) is silent. NG8115 is a compile-time extended diagnostic, so look for it in the build output or as an editor squiggle — the page itself renders fine.
Please provide the exception or error you saw
▲ [WARNING] NG8115: NG8115: The track function in the @for block should be invoked: row.field(/* arguments */). Find more at https://v21.angular.dev/extended-diagnostics/NG8115 [plugin angular-compiler]
libs/.../ng8115-repro.ts:14:4:
14 │ @for (row of rows(); track row.field) {
╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Please provide the environment you discovered this bug in (run ng version)
Angular CLI: 21.2.5
Node: 24.19.0
Package Manager: bun
Angular: 21.2.6
... animations, cdk, common, compiler, compiler-cli, core, forms
... material, platform-browser, router
Package Version
---------------------------------------------------------
@angular-devkit/build-angular 21.2.5
typescript 6.0.2
Anything else?
Still present on main at the time of filing:
- packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_track_function/index.ts is unchanged apart from using getTypeOfSymbol(symbol) in place of symbol.tsType; there is no FieldTree exemption.
- adev/src/content/guide/forms/signals/field-state-management.md still documents track field.
Workaround, for anyone who lands here: invoke it — track row.field(). The FieldTree is a Proxy over () => fieldNode with no apply trap, so the call just unwraps the proxy to the FieldNode it is permanently bound to. Proxy and node are one-to-one, so this partitions items identically to tracking the tree itself, and FieldState is not callable, so the check is satisfied.
Which @angular/* package(s) are the source of the bug?
compiler-cli, forms
Is this a regression?
No
Description
The NG8115 extended diagnostic (uninvokedTrackFunction) fires on the Signal Forms field-identity tracking that the docs recommend, whenever the FieldTree is reached through a property read instead of directly as the @for loop variable.
Field state management → Tracking values for array fields says:
That exact form is fine. But it is common to iterate a small view model that pairs each field with something else (an index for a remove button, a sibling value from the model, drag state), and then the very same field identity is expressed as row.field:
@for (row of rows(); track row.field) { ... } <!-- NG8115 -->Both loops track the identical object with the identical semantics; only the shape of the expression differs. The first is silent, the second warns.
Cause
UninvokedTrackFunctionCheck warns when the track expression is a PropertyRead/SafePropertyRead whose type has at least one call signature. FieldTree<T> is () => FieldState<T> plus proxied children, so it is callable — but the call signature is the state accessor, and its identity is precisely what the docs ask you to track. The check cannot distinguish "identity of a callable object" from "forgot to invoke the track function".
The bare-loop-variable case escapes only by accident: SymbolBuilder.getSymbolOfTemplateExpression resolves it via getExpressionTarget to a SymbolKind.Variable, and the check requires SymbolKind.Expression. So the exemption is a symbol-kind artifact, not a carve-out for Signal Forms — which is why the documented shape passes and every other spelling of the same intent does not.
The suggested fix in the message is actively harmful here: invoking with arguments is meaningless for a FieldTree, and the two obvious ways to make the warning go away — tracking by $index, or by a value such as row.field().value() — are exactly the patterns Signal Forms warns against (see #66711, #66796). Tracking by index reuses stateful inputs across a delete and moves focus to the wrong row.
Suggested fix
Skip the check when the track expression's type is a FieldTree (or, more generally, when the type is callable with zero required parameters and is not a template reference to a method), so that field-identity tracking is accepted regardless of how the field is reached.
Please provide a link to a minimal reproduction of the bug
https://stackblitz.com/edit/stackblitz-starters-hedc1b4t?file=src%2Fmain.ts
The whole repro is the single component in src/main.ts. It produces exactly one NG8115, pointing at loop B; loop A (the documented shape) is silent. NG8115 is a compile-time extended diagnostic, so look for it in the build output or as an editor squiggle — the page itself renders fine.
Please provide the exception or error you saw
▲ [WARNING] NG8115: NG8115: The track function in the @for block should be invoked: row.field(/* arguments */). Find more at https://v21.angular.dev/extended-diagnostics/NG8115 [plugin angular-compiler] libs/.../ng8115-repro.ts:14:4: 14 │ @for (row of rows(); track row.field) { ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Please provide the environment you discovered this bug in (run ng version)
Anything else?
Still present on main at the time of filing:
Workaround, for anyone who lands here: invoke it — track row.field(). The FieldTree is a Proxy over () => fieldNode with no apply trap, so the call just unwraps the proxy to the FieldNode it is permanently bound to. Proxy and node are one-to-one, so this partitions items identically to tracking the tree itself, and FieldState is not callable, so the check is satisfied.