Validation — Accumulating Errors
We have all experienced a frustrating user interface pattern: you fill out a long form, hit submit, and are presented with a red validation error indicating your password is too short. You fix it, submit again, only to be told that your email address is malformed. You fix that, submit once more, and receive a third warning about your postal code.
This trial-and-error cycle is the direct result of a design choice in our code. When we use standard
try/catch blocks, conditional guards, or Result containers to validate input, we are using a
fail-fast model. This model short-circuits at the very first failure:
If validateName fails, the execution stops. The subsequent checks for email and age are never
even evaluated. While this short-circuiting behavior is correct for sequential operations where step
B depends on the success of step A, it is highly unhelpful for validating independent data fields in
forms, API payloads, or configuration files.
Validation<E, A> is a data structure designed specifically to address this problem. It represents
either a Passed<A> success or a Failed<E> failure containing a list of accumulated errors.
Instead of short-circuiting, it runs all checks independently and merges all errors together.
Creating Validations
Section titled “Creating Validations”To begin validating, we lift our data checks into the Validation context:
Under the hood, all failures in Validation are collected inside a NonEmptyList (a type-safe
array guaranteed to contain at least one element). When you call Validation.failed(err), the
library automatically wraps your error in a list container.
If you already have an array of errors and want to lift it directly, you can use failedAll:
Constructing checks with fromPredicate
Section titled “Constructing checks with fromPredicate”You can build reusable, rule-specific validation checkers using fromPredicate:
The second argument receives the original input, allowing you to format descriptive, clear feedback for your users.
The Accumulation Pattern: ap
Section titled “The Accumulation Pattern: ap”What makes Validation structurally different from Result is how we combine multiple independent
checks.
The primary tool for this is ap (short for apply). The pattern begins by wrapping a curried
constructor function in passed, and then applying each validated argument one-by-one:
Let’s dissect what happens when this pipeline executes. Each ap step inspects both sides:
- If both the function and the argument have passed, the argument value is applied to the function.
- If either the function or the argument has failed, the errors are gathered.
- If both have failed, their respective error lists are merged.
Because each argument is validated independently before being combined, all validation checks are
guaranteed to run, and every failure is gathered into a single consolidated Failed container.
Alternative Combinators: product and productAll
Section titled “Alternative Combinators: product and productAll”If the curried ap pattern feels unfamiliar or syntactically complex, Validation provides
simpler, array-based alternatives.
Combining two checks with product
Section titled “Combining two checks with product”product takes two independent validations and merges them into a single Validation carrying a
tuple of both values:
If either validation has failed, the errors from both sides are collected and merged.
Combining many checks with productAll
Section titled “Combining many checks with productAll”productAll accepts an array of validations, runs all of them, and returns either a Passed tuple
containing all successfully validated values, or a Failed list containing every accumulated error:
Because productAll expects a NonEmptyList (a non-empty array) of validations, you are guaranteed
to receive a compiled result carrying a type-safe array of values, completely avoiding the
possibility of empty array inputs or undefined states at compile time.
Transforming values
Section titled “Transforming values”You can transform the success value inside a Passed container without worrying about the failure
branch using map:
If the validation has failed, map does nothing and lets the accumulated errors propagate.
Extracting the value
Section titled “Extracting the value”Once all checks have run and the errors have been accumulated, you must exit the Validation
context 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 returns a safe fallback value
if the validations failed:
As with other modules in this library, getOrElse expects a function (a thunk) to defer evaluating
the fallback value, saving execution costs if the validation checks pass successfully.
Exhaustive matching with match and fold
Section titled “Exhaustive matching with match and fold”To drive distinct UI rendering or business branches based on the outcome, you can analyze both cases
using match or fold:
Side effects with tapError
Section titled “Side effects with tapError”When you want to log or inspect validation failures mid-pipeline without altering the validation
flow, you can use tapError. It executes a side-effectful callback only if the validation has
failed, passing the full list of accumulated errors:
Fallback strategies: recover
Section titled “Fallback strategies: recover”recover provides a fallback Validation when validation has failed. It passes the accumulated
error list to your fallback function, allowing you to inspect what went wrong and decide how to
recover dynamically:
Interoperability and Hand-offs
Section titled “Interoperability and Hand-offs”Because software systems use a variety of modeling types, you can translate Validation to and from
other modules.
Discarding errors to Maybe
Section titled “Discarding errors to Maybe”If you only care about obtaining a valid value and do not need to report the reasons for failure,
you can downgrade the Validation to a Maybe using toMaybe:
Bridging from Result
Section titled “Bridging from Result”When incorporating an operation that throws or fail-fast checks (like a Result parser) into an
accumulating validation flow, you can lift it using fromResult:
The single error from the Err is wrapped in a type-safe NonEmptyList automatically.
Sequencing actions by converting to Result
Section titled “Sequencing actions by converting to Result”Validation is outstanding for running parallel, independent checks. However, once you have
established that the data is 100% valid, you typically need to run sequential side effects that can
fail (like saving to a database, sending a request, or writing to disk).
For this, you should hand off execution to a Result pipeline using toResult:
This hand-off represents a highly common, elegant pattern in production applications: use
Validation to gather all input friction, convert to Result once the data is clean, and use
Result.chain to sequence sequential database or network actions.
Why Validation has no bind / bindTo
Section titled “Why Validation has no bind / bindTo”A developer familiar with other modules in this library (like Result or Maybe) might wonder why
Validation completely omits bind and bindTo helpers.
This omission is a deliberate architectural and structural design choice.
bind and bindTo represent monadic sequencing. In a sequential pipeline:
Step B (prefs) requires the successful output of Step A (user). If Step A fails, Step B cannot
run. This sequential dependency naturally defeats the primary, first-class purpose of Validation —
independent error accumulation. If steps are dependent, we cannot evaluate them in parallel, and
we cannot gather all errors across all branches at the same time.
For sequenced pipelines that depend on prior steps, you should use Result (which naturally
supports bind and bindTo for fail-fast chaining). If you need to validate independent inputs,
keep them in Validation. Once the data is validated, you can cleanly hand it off to a Result
pipeline using Validation.toResult to perform sequential actions.
Combining records: struct
Section titled “Combining records: struct”To combine multiple independent validation checks into a single validated object, you can use
Validation.struct.
Unlike Result.struct or Maybe.struct (which short-circuit on the first failure),
Validation.struct accumulates errors from all failed branches into a single Failed list:
If all validation fields pass successfully, it returns a Passed container containing the fully
constructed record:
When to use Validation vs Result
Section titled “When to use Validation vs Result”Use Validation when:
Section titled “Use Validation when:”- Checks are independent: Validating form fields, structural payload parsing, or configuration sheets.
- You need comprehensive feedback: You want to display all errors at once to a user or log them all to an audit sheet.
- The combinations are parallel: You are fanning out data to multiple checkers simultaneously.
Use Result when:
Section titled “Use Result when:”- Checks are dependent: Validating step B requires step A to have succeeded (e.g. validating an address requires the user record to have been successfully fetched first).
- You want to fail-fast: Halting execution immediately at the first sign of friction is the desired control flow behavior.
- The operation is a side effect: Writing files, connecting to networks, or querying databases.