Validation — collecting errors
If you’ve ever fixed a validation error on a form, resubmitted, and been told about a different
error — you’ve felt the frustration of validation that stops at the first failure.
Validation<E, A> runs all checks and collects every failure in one pass. Users see everything
wrong at once, not one problem at a time.
The problem with short-circuiting
Section titled “The problem with short-circuiting”When validating a form, Result’s behavior is unhelpful:
If validateName fails, the pipeline stops. The user sees one error, fixes it, submits again, and
sees the next one. You want to show all three errors at once.
The Validation approach
Section titled “The Validation approach”Validation accumulates errors across independent checks. When you combine two invalid validations,
both error lists are merged:
Running both checks with ap collects all failures:
If both pass, you get the assembled value:
Creating Validations
Section titled “Creating Validations”invalid wraps a single error in a list. invalidAll accepts a NonEmptyList directly, useful
when you already have a collection of errors.
How ap accumulates errors
Section titled “How ap accumulates errors”ap is what sets Validation apart from Result. The pattern is: start with your constructor
function wrapped in Validation.valid, then apply each validated argument with ap:
Each ap step:
- If both sides are valid, applies the function to the value
- If either side is invalid, merges both error lists into a single
Invalid
The key property: all ap steps run regardless of prior failures. This is what allows all errors to
be collected.
The ap pattern looks unusual at first. A simpler read: you’re lifting a curried constructor and
applying each validated argument one by one. If the constructor shape feels awkward, productAll
often expresses the same intent more directly — pass a list of validations, get back a list of valid
values.
Combining two validations with product
Section titled “Combining two validations with product”product takes two independent validations and combines them into a single Validation holding a
tuple of both values. If either fails, errors from both sides are merged:
This is the binary building block for combining two independent checks when you want both values afterwards.
Combining many validations with productAll
Section titled “Combining many validations with productAll”productAll takes a non-empty list of validations, runs all of them, and either collects all values
or accumulates all errors:
Because the input is a NonEmptyList, the empty-array case is a compile-time error — the return
type is always Validation<E, readonly A[]> with no undefined.
Transforming values with map
Section titled “Transforming values with map”map transforms the valid value, leaving Invalid untouched:
Extracting the value
Section titled “Extracting the value”getOrElse — provide a fallback as a thunk () => B, called only when the result is
Invalid. The fallback can be a different type, widening the result to the union of both:
match — handle each case explicitly. The invalid handler receives the full error list. fold
is the positional form — invalid handler first, valid handler second:
Recovering from Invalid
Section titled “Recovering from Invalid”recover provides a fallback Validation when the result is Invalid. The fallback receives the
accumulated error list, so you can inspect which errors occurred and decide how to recover:
When the input is already Valid, the fallback is never called:
How it compares to Result
Section titled “How it compares to Result”Result — stops at first error
sequenceDiagram
participant P as your code
participant N as check name
participant E as check email
participant A as check age
P->>N: name = ""
N-->>P: Err("Name required")
Note over E,A: never called
Validation — all checks run
sequenceDiagram
participant P as your code
participant N as check name
participant E as check email
participant A as check age
P->>N: name = ""
N-->>P: Invalid("Name required")
P->>E: email = "bad"
E-->>P: Invalid("Invalid email")
P->>A: age = -1
A-->>P: Invalid("Age must be >= 0")
Note over P: Invalid(["Name required", "Invalid email", "Age must be >= 0"])
When to use Validation vs Result
Section titled “When to use Validation vs Result”Use Validation when:
- You’re validating multiple independent fields and want all errors at once
- The consumer of your output (e.g., a form UI) needs the complete list of what went wrong
Use Result when:
- Each step depends on the previous one succeeding — convert to
Resultand useResult.chainfor dependent validation, then convert back - You want to fail fast and stop processing as soon as something goes wrong
- The operation isn’t about validation — it’s about control flow
In practice, many real-world scenarios mix both: use Validation to check individual fields, then
use Result to sequence the side effects once the data is known to be valid.