Overview
Like Result, but collects *all* errors instead of stopping at the first one. Perfect for form validation where users need to see every field error at once.
The Problem: "Fix one error, submit, find another error"
The Frustrating User Experience
You've experienced this as a user filling out a form:
// Traditional validation with try/catch or Result
function validateForm(data) {
if (data.email.length === 0) throw new Error('Email required')
if (!data.email.includes('@')) throw new Error('Invalid email') // Never reached!
if (data.password.length < 8) throw new Error('Password too short') // Never reached!
if (!/[A-Z]/.test(data.password)) throw new Error('Need uppercase') // Never reached!
return data
}
// User sees: "Email required"
// User fixes email, submits again
// User sees: "Invalid email"
// User fixes email, submits again
// User sees: "Password too short"
// User fixes password, submits again
// User sees: "Need uppercase"
// ๐ค User gives upThe user had 4 errors but only learned about them one at a time.
How Modern Products Handle This
Stripe Checkout (actual behavior):
When you submit invalid payment info, Stripe shows:
โ Email address is invalid
โ Card number is incomplete
โ Expiration date is invalid
โ CVC is incomplete
All at once. You fix everything, then submit.GitHub Pull Request (actual behavior):
When PR validation fails:
โ Title cannot be empty
โ At least one reviewer is required
โ All CI checks must pass
โ Branch has merge conflicts
All errors shown immediately.Vercel Deployment (actual behavior):
Build validation errors:
โ Missing environment variable: DATABASE_URL
โ Missing environment variable: API_KEY
โ Invalid Node version: requires >=18.0.0
All configuration errors at once.The Solution: Validation with Error Accumulation
import { validate, fromPredicate } from '@ontologyio/receta/validation'
// Validation accumulates ALL errors
const validatePassword = (password: string) =>
validate(password, [
fromPredicate(s => s.length >= 8, 'At least 8 characters'),
fromPredicate(s => /[A-Z]/.test(s), 'At least one uppercase'),
fromPredicate(s => /[0-9]/.test(s), 'At least one number'),
fromPredicate(s => /[^A-Za-z0-9]/.test(s), 'At least one special char')
])
validatePassword('weak')
// => Invalid([
// 'At least 8 characters',
// 'At least one uppercase',
// 'At least one number',
// 'At least one special char'
// ])
// User sees ALL 4 errors immediately! โจMulti-Field Forms
Give schema one validator per field. Passing validate only a list of validators returns a validator, which is how several rules share a field.
type SignupForm = {
email: string
password: string
age: number
country: string
}
const validateForm = (form: SignupForm) =>
schema({
email: validate([required('Email is required'), email()]),
password: minLength(8, 'At least 8 characters'),
age: min(18),
country: oneOf(['US', 'UK', 'CA', 'AU'], 'Invalid country')
}, form)
validateForm({ email: '', password: 'weak', age: 15, country: 'XX' })
// => Invalid([
// { field: 'email', errors: ['Email is required', 'Invalid email address'] },
// { field: 'password', errors: ['At least 8 characters'] },
// { field: 'age', errors: ['Must be at least 18'] },
// { field: 'country', errors: ['Invalid country'] }
// ])
// Shows ALL field errors at once!Validation vs Result: When to Use Which?
Result: Fail-Fast (Stop at First Error)
import { collect, ok, err } from '@ontologyio/receta/result'
// collect stops at first error
collect([
ok(1),
err('error1'), // Stops here!
err('error2'), // Never checked
err('error3') // Never checked
])
// => Err('error1')Use Result when:
- โ Business logic validation (auth, permissions)
- โ Sequential operations (database โ API โ cache)
- โ Early termination is desired
- โ First error is enough context
Example: User authentication
import { flatMap } from '@ontologyio/receta/result'
pipe(
validateToken(token), // Stop if token invalid
flatMap(getUser), // Stop if user not found
flatMap(checkPermissions) // Stop if no permission
)
// No point checking permissions if token is invalid!Validation: Error Accumulation (Collect All Errors)
import { collectErrors, invalid } from '@ontologyio/receta/validation'
// collectErrors accumulates all errors
collectErrors([
invalid(['error1']),
invalid(['error2']),
invalid(['error3'])
])
// => Invalid(['error1', 'error2', 'error3'])Use Validation when:
- โ Form validation (show all field errors)
- โ API request validation (return all invalid fields)
- โ Bulk data validation (CSV import, batch processing)
- โ Configuration validation (all missing env vars)
Example: Form submission
// User needs to see ALL errors to fix the form
collectErrors([
validateEmail(form.email), // Check all fields
validatePassword(form.password),
validateUsername(form.username),
validateCountry(form.country)
])
// All errors at once, better UX!Comparison Table
| Feature | Result | Validation |
|---|---|---|
| Error handling | Single error | Array of errors |
| Behavior | Fail-fast (stops at first) | Accumulates all |
| Best for | Sequential operations | Independent fields |
| Use case | Business logic, API calls | Forms, bulk validation |
| Example | Auth โ DB โ Process | Form with 10 fields |
Real-World Use Cases
1. Form Validation
Problem: Registration form with multiple fields. Solution: Show all validation errors at once.
const validateRegistration = (form: RegistrationForm) =>
schema({
username: validate([required('Username is required'), minLength(3), maxLength(20)]),
email: validate([required('Email is required'), email()]),
password: validate([
required('Password is required'),
minLength(8),
matches(/[A-Z]/, 'Need an uppercase letter'),
matches(/[0-9]/, 'Need a number')
]),
age: validate([min(18), max(120)]),
terms: equals(true, 'You must accept the terms')
}, form)
// Shows all invalid fields immediately
validateRegistration(formData)2. API Request Validation
Problem: REST API needs to validate request body. Solution: Return all validation errors in single response.
app.post('/api/users', (req, res) => {
const validation = validateCreateUser(req.body)
match(validation, {
onValid: (user) => res.status(201).json(user),
onInvalid: (errors) => res.status(400).json({ errors })
})
})
// Response shows all field errors:
// { errors: [
// { field: 'email', errors: ['Invalid email'] },
// { field: 'age', errors: ['Must be 18+'] }
// ] }3. CSV Import / Bulk Validation
Problem: Importing 1000 rows from CSV. Solution: Get complete error report for all invalid rows.
validateAll(csvRows, validateProduct)
// One run returns the errors from every invalid row
// Show user: "Rows 5, 23, 157, 892 have errors"
// Instead of: "Row 5 has error" (then rerun to find row 23...)4. Configuration Validation
Problem: App startup with environment variables. Solution: Show all missing/invalid config at once.
const validateConfig = (config: AppConfig) =>
schema({
DATABASE_URL: validate([required('DATABASE_URL is required'), url()]),
API_KEY: validate([required('API_KEY is required'), minLength(32)]),
PORT: validate([integer(), between(1000, 65535)]),
NODE_ENV: oneOf(['development', 'production', 'test'])
}, config)
// Environment variables can be missing and are always strings
validateConfig({
DATABASE_URL: process.env.DATABASE_URL ?? '',
API_KEY: process.env.API_KEY ?? '',
PORT: Number(process.env.PORT),
NODE_ENV: process.env.NODE_ENV ?? ''
})
// Shows ALL missing env vars, not just first one5. Multi-Step Wizards
Problem: 5-step wizard, validate all steps before final submit. Solution: Accumulate errors across all steps.
const validateWizard = (data: WizardData) =>
schema({
step1: validateStep1,
step2: validateStep2,
step3: validateStep3,
step4: validateStep4,
step5: validateStep5
}, data)
validateWizard(wizardData)
// Each error's `field` names a step that failed
// Show: "Steps 2 and 4 have errors"
// User can jump directly to those stepsMental Model: Quality Control Inspector
Think of Validation like a quality control inspector checking products on an assembly line:
Result (Fail-Fast Inspector):
Inspector checks products one by one:
โ Product 1: OK
โ Product 2: Defect found! STOP LINE!
(Products 3, 4, 5 never checked)
Good when: One defect means stop everythingValidation (Thorough Inspector):
Inspector checks ALL products first:
โ Product 1: OK
โ Product 2: Defect A
โ Product 3: OK
โ Product 4: Defects B, C
โ Product 5: Defect D
Report: "Found 4 defects across 3 products: A, B, C, D"
Good when: Need complete report before fixingCore Concepts
1. Error Accumulation
// Multiple validators on single value
validate(password, [
validator1, // โ Fails
validator2, // โ Fails
validator3 // โ Passes
])
// => Accumulates errors from validator1 AND validator22. Field-Level Errors
// Object validation with field tracking
schema(userSchema, userData)
// => Invalid([
// { field: 'email', errors: ['Invalid'] },
// { field: 'age', errors: ['Too young'] }
// ])
// Knows WHICH fields failed3. Composable Validators
// Build complex validators from simple ones
const strongPassword = validate([
required('Password is required'),
minLength(8),
matches(/[A-Z]/, 'Need an uppercase letter'),
matches(/[0-9]/, 'Need a number'),
matches(/[^A-Za-z0-9]/, 'Need a special character')
])
// Reuse across your appWhen to Use Validation
โ Use Validation When:
- Forms with multiple fields - Show all errors at once
- API request validation - Return complete error details
- Bulk operations - Process all items, report all failures
- Independent validations - Each check is unrelated to others
- User needs full feedback - Better UX with complete error list
โ Don't Use Validation When:
- Sequential dependencies - Later steps depend on earlier ones
- Early termination needed - First error is enough
- Performance critical - Don't want to run unnecessary checks
- Single validation - No need for accumulation
- Business logic - Use Result instead
Examples:
import { flatMap } from '@ontologyio/receta/result'
// โ
Good: Form validation (independent fields)
schema(formSchema, formData)
// โ Bad: Authentication chain (sequential dependencies)
// Use Result instead:
pipe(
validateToken(token),
flatMap(loadUser),
flatMap(checkPermissions)
)
// โ
Good: API batch validation
validateAll(requests, validateRequest)
// โ Bad: File processing pipeline
// Use Result instead:
pipe(
readFile(path),
flatMap(parseJSON),
flatMap(validateSchema),
flatMap(transform)
)What's Next?
Now that you understand why Validation exists, learn how to use it:
- Constructors - Creating validations with
valid,invalid,fromPredicate - Validators - Built-in validators for common cases
- Combinators - Combining validations with
collectErrors,validate,schema - Patterns - Real-world recipes for forms, APIs, bulk validation
- Migration - Moving from try-catch, Zod, or Yup
- API Reference - Complete function reference, including the transformers
map,mapInvalidandflatMap
Or jump straight to Common Patterns for copy-paste examples!