Validation — Accumulating Errors
1.56 kB minified + gzipped
When validating incoming data — such as a user registration form, a configuration file, or an API request body — a payload often contains multiple invalid fields simultaneously. Standard JavaScript error-handling patterns rely on exceptions or early return statements:
This fail-fast approach short-circuits on the first failure. If a user submits a form with an invalid username, an invalid email, and an invalid age, the function throws immediately on the username. The caller never discovers the email and age errors until the user corrects the username and submits again. In web forms, this forces users into a frustrating cycle of submitting, fixing one field, submitting again, and finding another error.
Developers often attempt to solve this by creating an imperative error array:
While this accumulates all errors, it loses composition. Combining checks across multiple functions requires manually merging arrays, passing error lists through arguments, and handling boilerplate status checks at every call site.
Validation<E, A> models validation outcomes as a discriminated union: Passed<A> representing a
valid value, or Failed<E> representing an accumulated list of errors. Instead of short-circuiting
on the first failure, it evaluates independent checks and merges all accumulated errors together.
flowchart TB
Check1(["Check Username"]) -->|"Passed('alex') or Failed(['Too short'])"| Combine{"Validation.product / apply"}
Check2(["Check Email"]) -->|"Passed('a@b.com') or Failed(['Invalid domain'])"| Combine
Check3(["Check Age"]) -->|"Passed(25) or Failed(['Must be 18+'])"| Combine
Combine -->|"All checks pass"| Passed["Validation.Passed(User)"]
Combine -->|"Any checks fail"| Failed["Validation.Failed(NonEmptyErrors)"]
Failed -.->|"All errors accumulated, never short-circuited"| Note["Complete Error Report"]
Failures in Validation are stored inside an Arr.NonEmpty<E> (an array guaranteed to hold at
least one element). This prevents empty error states at compile time.
Creating Validations
Section titled “Creating Validations”Validation instances represent either a verified value or an initial failure.
When you call Validation.make.failed(err), the library automatically wraps the error in a
single-element Arr.NonEmpty array.
If you already have an array of errors, wrap it directly with failedAll:
Constructing checks with from.Predicate, from.nullable, and tryCatch
Section titled “Constructing checks with from.Predicate, from.nullable, and tryCatch”Build reusable rule checkers from boolean conditions using from.Predicate:
The second argument receives the original input, allowing you to format descriptive error messages.
To validate that an input is not null or undefined, use Validation.from.nullable:
When a validation step calls a parser that throws on invalid input (such as new URL(...) or
JSON.parse), use Validation.tryCatch:
Inspecting with type guards: is.passed and is.failed
Section titled “Inspecting with type guards: is.passed and is.failed”When inspecting a Validation outcome imperatively, use Validation.is.passed and
Validation.is.failed:
Combining Validations: struct and apply
Section titled “Combining Validations: struct and apply”Combining multiple independent checks is the central capability of Validation.
Combining records with struct
Section titled “Combining records with struct”Validation.struct combines a record of independent validation checks into a single validated
object. Unlike Result.struct (which halts on the first error), Validation.struct evaluates every
property and accumulates all errors into a unified list:
When all fields pass, it produces a Passed record with the assembled values:
Combining positional arguments with apply
Section titled “Combining positional arguments with apply”When constructing objects from curried functions or assembling tuple arguments, use
Validation.apply:
Each apply step checks both sides:
- If both the function and argument pass, the argument is applied.
- If either side fails, the errors are gathered into
Failed. - If both sides fail, their error lists are concatenated together.
Transforming values and errors
Section titled “Transforming values and errors”You can transform the success value inside a Passed container without altering failure branches
using map:
If the validation fails, map does nothing and lets accumulated errors flow through.
To transform accumulated errors — such as prefixing field names when nesting child validations — use
Validation.mapError:
Extracting the value
Section titled “Extracting the value”Once all checks have run and errors are collected, extract the value at the edge of your pipeline.
Safe fallbacks with getOrElse
Section titled “Safe fallbacks with getOrElse”getOrElse extracts the validated value from a Passed container, or evaluates a fallback thunk if
the validation failed:
getOrElse expects a thunk to avoid computing the fallback value when checks pass.
Exhaustive matching with match
Section titled “Exhaustive matching with match”To handle both outcomes explicitly, use match:
match enforces exhaustive handling with named cases. Positional fold callbacks are not supported.
Side effects with tap and tapError
Section titled “Side effects with tap and tapError”To log or inspect outcomes mid-pipeline without altering the validation flow, use tap for valid
values and tapError for accumulated errors:
Fallback strategies: recover and recoverUnless
Section titled “Fallback strategies: recover and recoverUnless”recover provides an alternative Validation when validation has failed. It passes the accumulated
error list to your fallback function:
When specific errors should prevent recovery (such as security violations or invalid credentials),
use Validation.recoverUnless. If any accumulated error matches the isBlocked predicate, the
failure propagates untouched:
Interoperability and Hand-offs
Section titled “Interoperability and Hand-offs”Validation translates cleanly to and from other containers in the library.
Converting to and from Maybe
Section titled “Converting to and from Maybe”To discard error details and retain only success or failure, convert Validation to Maybe using
to.Maybe:
To convert an optional Maybe into a Validation, supply an error generator for the None case
using Validation.from.Maybe:
Bridging from Result
Section titled “Bridging from Result”When incorporating a fail-fast parser into an accumulating validation flow, convert it using
from.Result:
The error from the Err variant is wrapped in an Arr.NonEmpty array automatically.
Sequencing actions by converting to Result
Section titled “Sequencing actions by converting to Result”Validation evaluates independent checks simultaneously. Once all inputs are verified, subsequent
steps (such as database writes or API calls) often depend on earlier outputs and must execute
sequentially.
To transition from parallel validation into sequential execution, convert to Result using
to.Result:
Calling Validation.to.Result without arguments retains the raw Arr.NonEmpty<E> error list inside
Err.
Why Validation omits chain, assign, and assignTo
Section titled “Why Validation omits chain, assign, and assignTo”Developers familiar with Result or Maybe often ask why Validation lacks chain, assign, and
assignTo.
chain and assign sequence dependent operations where Step B requires the output of Step A:
If Step A fails, Step B cannot run because it lacks an input. This sequential dependency directly contradicts error accumulation: you cannot run checks in parallel if one check requires the value of another.
For dependent steps that rely on prior results, use Result. For independent inputs that can all be
evaluated at once, validate them with Validation.struct or Validation.apply, and then hand off
the verified record to Result using Validation.to.Result.
Field-level validation: Validation.keyed
Section titled “Field-level validation: Validation.keyed”While Validation.struct combines all errors into a single list, user interfaces often require
errors partitioned by input field so an error message renders next to the matching input.
Validation.keyed keeps individual validation results partitioned by key inside an object:
Every property in formState is an independent Validation container, allowing UI components to
render field errors independently.
Inspecting field outcomes
Section titled “Inspecting field outcomes”Query the overall status of the keyed object using boolean guards:
Extracting values and errors with Maybe
Section titled “Extracting values and errors with Maybe”To extract the clean data or the field-level errors, use getPassed and getErrors:
To collapse the keyed object into a single consolidated Validation<E, T>, pass the object directly
into Validation.struct:
Asynchronous Validation: Task.Validation
Section titled “Asynchronous Validation: Task.Validation”When validation checks require asynchronous operations — such as checking username availability in a
database or verifying an address against an external postal API — use Task.Validation<E, A>.
Task.Validation is an alias for Task<Validation<E, A>>. It runs independent asynchronous checks
concurrently and aggregates all failures into a single list:
If any check fails, the task resolves to a Failed variant containing the accumulated list of all
errors.
Transforming and inspecting errors in tasks
Section titled “Transforming and inspecting errors in tasks”Format or log asynchronous validation errors mid-pipeline using dedicated combinators:
Pipeline composition
Section titled “Pipeline composition”Here is an end-to-end user registration endpoint pipeline. It validates input fields independently,
namespaces errors, hands off the verified record to a sequential Result pipeline for database
persistence, and formats the final HTTP response:
Problems it solves
Section titled “Problems it solves”- Multi-field web form validation: Web forms frequently receive multiple invalid fields in a
single submission. Short-circuiting error handlers stop on the first error, forcing repetitive
submit-fix cycles.
Validationaggregates all field errors into a single collection, allowing user interfaces to render feedback on every invalid input at once. - Batch data ingestion and bulk import diagnostics: When importing spreadsheets, configuration
files, or batch API payloads, operators need an exhaustive report of all invalid rows.
Validation.structandValidation.applyevaluate all checks independently, gathering every schema and domain violation into a single diagnostic list rather than halting on row one. - Multi-rule entity policy checks: When evaluating complex entity constraints — such as password
strength requirements, credit assessment rules, or compliance checklists —
Validationevaluates all criteria independently and reports every unmet condition simultaneously. - Bridging validation accumulation with fail-fast execution (
Validation.to.Result): In web endpoints, validating an incoming request body requires accumulating all field errors withValidation, while subsequent database operations require fail-fast sequential chaining withResult.Validation.to.Resulttransitions between these patterns cleanly, preserving all collected errors while entering standard sequential pipelines. - Contextual error tagging across nested forms (
Validation.mapError): In nested forms and multi-step wizards, child components validate their own sub-fields.Validation.mapErrorallows parent forms to namespace child errors (e.g. prefixingaddress.zipCode) before merging, keeping error messages structured and localized.