Validation error types
Choose an error type according to where the error is in the validation pipeline: a rule returns a result, the node assigns ownership, and an error renderer consumes the published errors.
Choose a result or error contract
| Task | Public type |
|---|---|
| Describe the base category and optional message | ValidationError |
| Return a synchronous rule result | ValidationResult |
| Return an error that may target another node | ValidatorError |
| Return an error for the current node without attribution | ValidationErrorWithoutTargetNode |
| Accept an error before or after node attribution | ValidationErrorWithOptionalTargetNode |
| Consume a published error with an owning node | ValidationErrorWithTargetNode |
| Represent successful validation | ValidationSuccess |
| Return asynchronous results | AsyncValidationResult |
| Return errors or further synchronous validator composition | ComposableValidationResult |
ValidationError exposes kind: string and message?: string. Do not assume every error has a
message; provide an application fallback or a configured validator message. A synchronous result
can also be a string, which becomes a custom error, or a readonly array of messages and errors.
An empty string is still an error message. null, undefined, and void indicate success.
ValidatorError accepts kind: string | number as input. A numeric identifier such as 123
is normalized to '123' before publication; ValidationError and error queries continue to
use strings. Use ValidatorError or ValidationResult to annotate rules returning numeric
kinds, and query them with getError('123'). Numeric errors are shallow copies; their source
object is not mutated. See the complete validator result contract
for the declaration callback's intentional any return and checked authoring alternatives.
Structured built-in and custom errors
| Requirement | Public type |
|---|---|
| A union of library-provided errors | BuiltInValidationError |
| An unregistered custom error with unknown additional data | CustomValidationError |
| The error shape for a particular kind | ValidationErrorForKind<TKind> |
| Register custom kinds through module augmentation | ValidationErrorMap |
hasError(kind) and getError(kind) autocomplete built-in kinds and keys added through
ValidationErrorMap module augmentation. Both still accept unregistered names and dynamic
strings. Suggestions are independent of the node's active errors or installed validators.
Use getError(kind) when you need kind-specific fields. The base error collection guarantees
category, optional message, and ownership; it does not make every validator-specific property
available on every entry. Register application error kinds to give lookups their precise shape.
Module augmentation adds compile-time information; the validator must still produce matching data.
import { field, form, min, type ValidationError } from '@ngblocks/form-nodes';
declare module '@ngblocks/form-nodes' {
interface ValidationErrorMap {
profileReservedName: ValidationError & {
readonly kind: 'profileReservedName';
readonly suggestion: string;
};
}
}
const profile = form({
age: field(16, [min(18)]),
name: field('admin', [({ value }) => {
return value() === 'admin'
? { kind: 'profileReservedName', suggestion: 'Choose a personal name.' }
: null;
}]),
});
const minimum = profile.age.getError('min');
minimum?.min; // 18
minimum?.actual; // 16
if (minimum?.min !== 18 || minimum.actual !== 16) throw new Error('Built-in lookup must expose structured data.');
const reserved = profile.name.getError('profileReservedName');
reserved?.suggestion; // 'Choose a personal name.'
if (reserved?.suggestion !== 'Choose a personal name.') throw new Error('Registered custom data must remain available.');
if (reserved.targetNode !== profile.name) throw new Error('The pipeline must assign the error to its owning node.');
if (!profile.allErrors().some(error => error.targetNode === profile.name)) throw new Error('Aggregate errors must preserve ownership.');
profile.name.set('Ada');
if (profile.name.getError('profileReservedName') !== undefined) throw new Error('Correcting the value must remove the error.');
The built-in contracts include the data relevant to their rule, such as numeric or date bounds, actual values, patterns, word or length constraints, and duplicate indexes. Follow the individual validator references for exact error kinds and behavior.
Ownership and binding attribution
targetNode identifies the node owning an error. Reading an error through an ancestor's
allErrors() preserves its original target. A binding-specific error may also contain formNode,
the concrete directive binding that produced it; ordinary custom validators do not assign that
binding field themselves.
Use AnyNode and $api when handling target nodes whose child names are
unknown. errors() and allErrors() differ in their traversal scope; see
errors and validation status for own versus descendant errors,
visibility, and error-query behavior.
State and display integration
ValidationStatus describes valid, invalid, or unresolved state;
it is not an error object. Pending asynchronous work and existing errors must be considered
according to the node's aggregation rules.
For controls observing Angular and Form Nodes bindings through useFormNodeState(), use
ControlStateError. For translated or configured messages, use
ValidatorMessages and
ValidatorMessageParameters.
See validator messages for message resolution and custom control contracts for reusable error-display integration.
Error queries
node.errors() reads only errors owned by that node. An invalid form can have no own errors when its children are invalid. Use node.errors({ descendants: true }) to collect own errors followed by descendant errors in structural tree order. node.allErrors() remains an equivalent shortcut and returns the same cached array.
import { computed } from '@angular/core';
import { field, form, required } from '@ngblocks/form-nodes';
const profile = form({
name: field('', [required]),
address: { city: field('', [required]) },
});
const summary = computed(() => profile.errors({ descendants: true }));
profile.errors().length; // 0
summary().length; // 2
summary()[0]?.targetNode === profile.name; // true
profile.allErrors() === summary(); // true
if (profile.errors().length !== 0) throw new Error('Child errors must not become own errors.');
if (summary().length !== 2) throw new Error('The summary must include both required fields.');
if (summary()[0]?.targetNode !== profile.name) throw new Error('Errors must retain their targets.');
if (profile.allErrors() !== summary()) throw new Error('Both queries must share their cached array.');
profile.name.set('Ada');
profile.address.city.set('Zurich');
summary().length; // 0
if (summary().length !== 0) throw new Error('The query must react to corrected descendants.');
No arguments, {}, and { descendants: false } all select own errors. The option accepts a reactive boolean, for example inside computed(() => profile.errors({ descendants: includeChildren() })). Reads track the selected existing signal; they do not create a new computed per call.
Every error preserves its original targetNode. Own reads retain the concrete node type; subtree reads use AnyNode, whose collision-safe API is accessed through targetNode.$api. Fields have no descendants, so both queries return the same errors. Disabled descendants, asynchronous validation, and dynamic child changes follow the existing allErrors() behavior.
The errors property remains assignable to Angular Signal and can still be passed directly to signal consumers. Its exported type is NodeErrorsSignal. These options apply to node errors, including .$api.errors; binding and useFormNodeState() error signals retain their own signatures.