Troubleshooting
Start with the symptom you can observe. Each solution links to the guide that explains the underlying behavior in more detail.
Find your symptomâ
| What you see | Start here |
|---|---|
Angular rejects [formNode] | Missing directive import or unsupported host |
| Typing does not update the value you read | Pending control values |
| The form is invalid but has no errors of its own | Own and descendant errors |
| An async check does not rerun as expected | Reactive validator dependencies |
| State follows the wrong row after reordering | Array identity |
| Reset keeps the edited value | Reset values explicitly |
| The submit action never runs | Submission checks |
For help configuring the error component, see its source examples, display options, and safe configuration defaults.
đ Angular does not recognize [formNode]â
Symptom: Angular reports that it cannot bind to formNode, or the directive does not run.
Solution: import FormNodeDirective in every standalone component that uses [formNode], or export it
from an NgModule imported by that component. The same import supports controls and native form
roots:
@Component({
imports: [FormNodeDirective],
template: `
<form [formNode]="form">
<input [formNode]="form.email" />
</form>
`,
})
export class AccountEditor {
form = form({ email: field('') });
}
No separate root-form directive is required. See Control binding.
đ A [formNode] host is rejectedâ
Symptom: development fails with formNode: the host must be a native form control, provide a signal custom control, or provide ControlValueAccessor.
Solution: bind fields directly to input, select, or textarea, or give the custom component
one supported Angular contract:
value = model(...)orchecked = model(...).ControlValueAccessorregistered throughNG_VALUE_ACCESSOR.- An automatically discovered signal-control component.
- A separate
value/valueChangeorchecked/checkedChangepair with experimentalbindInputOutputPairs: true. UsesyncInputsseparately for state inputs; false/null for bindInputOutputPairs leaves the whole pair connection inactive.
See Advanced custom controls for the supported shapes and precedence.
đ The node value has not changed after typingâ
Symptom: the control displays the latest text, but calling the node still returns its previous value.
Cause: the node has a numeric or 'blur' debounce. The control representation changes
immediately, while the committed model waits:
myForm.search.value.control(); // 'angular'
myForm.search(); // previous committed value
myForm.search.debouncing(); // true
Solution: normally, wait for the configured commit. Call myForm.search.flush() when an
explicit action must commit immediately. Blur, touch, and form submission also commit pending
control values. See Value flow and debounce.
đ¨ A form is invalid but errors() is emptyâ
Symptom: myForm.invalid() is true, but myForm.errors() returns [].
Solution: errors() contains only rules owned by that exact node. Use allErrors() for a form
summary that includes descendants:
myForm.errors(); // Form-level errors only.
myForm.allErrors(); // Form-level and descendant errors.
See Own versus descendant errors.
â A required array is still valid when emptyâ
Symptom: required does not reject [].
Cause: required treats an array as a present value; it does not validate its item count.
Solution: use minLength(1) when at least one item is required:
const myForm = form({
selectedTags: field<string[]>([], [minLength(1)]),
});
âŗ An asynchronous validator does not react as expectedâ
Symptom: changing a related signal does not rerun validation, or returning a newly allocated parameter object starts work more often than expected.
Solution: read reactive dependencies inside params, when, or the validator callback. A
params result is compared shallowly: primitive entries and stable references avoid redundant
runs, while a changed top-level entry schedules new validation.
asyncValidator(checkAvailability, {
params: () => ({
tenantId: activeTenantId(),
locale: activeLocale(),
}),
});
Form Nodes cancels stale work when dependencies or values change. See
asyncValidator() parameters.
âŗ Array rows keep the wrong touched or pending stateâ
Symptom: after replacing or reordering server data, interaction state appears attached to the wrong row.
Solution: configure a stable trackBy key or function so reconciliation follows domain
identity rather than position:
const people = array({
id: field(''),
displayName: field(''),
}, {
initialValue: initialPeople,
trackBy: 'id',
});
In an Angular @for, track the node instance: @for (person of people; track person). See
Complete reconciliation.
đ set(null) on an array does not leave a null valueâ
Symptom: calling myArray.set(null) produces [].
Cause: array() is a permanent structural container. null and undefined deliberately clear
its items instead of making the node nullable.
Solution: use field<Item[]>() if the complete array is one nullable value owned by a single
control. Use array() when each item needs its own node and state. See
Array field or array().
âŠī¸ Calling reset() did not restore the original valueâ
Symptom: interaction state clears, but the current value remains.
Cause: parameterless reset() retains committed values and clears touched, dirty, and pending
control state.
Solution: pass the value to restore:
myForm.reset({
displayName: '',
email: '',
});
See Reset.
đī¸ enable() does not make a node enabledâ
Symptom: the node remains disabled after calling enable().
Cause: an ancestor or reactive option still contributes another disabled reason. enable()
removes only the local imperative reason created by disable().
Solution: inspect disabledReasons() and remove or change the active source. The same layered
model applies to readonly and hidden state. See
Interaction and availability.
đ¨ Native form submission does not run the actionâ
Check these conditions:
- The native form has
[formNode]="form"and the component importsFormNodeDirective. - The node was created with
form(), notgroup(), and has anonSubmitcallback. A group binding remains functional but intentionally has no action to run. - The submit button has
type="submit". - Validation is not blocking submission. Submission marks the tree touched and resolves to
falsewhen invalid. - Another action is not already running; overlapping submissions resolve to
false.
form = form({
email: field('', [required, email]),
}, {
onSubmit: value => saveAccount(value),
});
See Form submission.
đŦ A custom validator message is not the one expectedâ
Message catalogs use nearest-wins precedence:
- Validator-local
message. - Closest form or array
validatorMessagescatalog. - Closest
createFormPrimitives()validator-message default. - Closest
provideFormNodesConfig()provider. configureGlobalFormNodes().- Built-in English message.
Check the higher-priority scopes before changing a global catalog. See Validator messages and internationalization.
đ Still investigating?â
Reduce the case to one node and inspect its callable value, value.control(), validationStatus(),
errors(), allErrors(), disabledReasons(), touched(), and dirty() as applicable. The
API overview maps each concern to its detailed reference, while
Common mistakes covers modeling choices that can look like runtime bugs.
When reproducing a problem, use the public-API patterns in Testing forms.