Skip to main content

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

TaskPublic type
Describe the base category and optional messageValidationError
Return a synchronous rule resultValidationResult
Return an error that may target another nodeValidatorError
Return an error for the current node without attributionValidationErrorWithoutTargetNode
Accept an error before or after node attributionValidationErrorWithOptionalTargetNode
Consume a published error with an owning nodeValidationErrorWithTargetNode
Represent successful validationValidationSuccess
Return asynchronous resultsAsyncValidationResult
Return errors or further synchronous validator compositionComposableValidationResult

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

RequirementPublic type
A union of library-provided errorsBuiltInValidationError
An unregistered custom error with unknown additional dataCustomValidationError
The error shape for a particular kindValidationErrorForKind<TKind>
Register custom kinds through module augmentationValidationErrorMap

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.

profile-errors.ts
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.

errors-descendants.example.ts
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.