Operation Chaining
Composing operations into clean, readable pipelines.
The Chaining Pattern
Execute a sequence of operations that can fail. Execution stops at the first error:
const result = Result.ok(userData)
.andThen(validateEmail) // Can fail
.andThen(checkPermissions) // Can fail
.andThen(saveToDatabase) // Can fail
.mapErr(logError) // Transform error if it occurs
// If any step fails, remaining steps are skippedCore Methods
map() — Transform Success Values
Transform without returning a Result:
Result.ok(5)
.map(x => x * 2) // Ok(10)
.map(x => x + 5) // Ok(15)
.unwrap() // 15andThen() — Chain Results
Use when each operation returns a Result:
function divide(a: number, b: number): Result<number, string> {
return b === 0
? Result.err('division by zero')
: Result.ok(a / b)
}
Result.ok(100)
.andThen(x => divide(x, 2)) // Ok(50)
.andThen(x => divide(x, 5)) // Ok(10)
.andThen(x => divide(x, 0)) // Err('division by zero')
.unwrapOr(0) // 0orElse() — Recover from Error
Execute recovery when an error occurs:
fetchFromCache(key)
.orElse(() => fetchFromDatabase(key))
.orElse(() => fetchFromAPI(key))
.unwrapOr(defaultData)Conditional recovery:
operation()
.orElse((error) => {
if (error.code === 404) return Result.ok(defaultValue)
if (error.code === 500) return Result.err(error) // Re-throw
return Result.ok(null)
})Filtering Values
filter() — With Default Error
Filter an Ok value based on a predicate. Returns Ok if it passes, converts to Err if it fails:
Result.ok(10).filter((x) => x > 5)
// Ok(10)
Result.ok(3).filter((x) => x > 5)
// Err(Error: Filter predicate failed)
// With custom message
Result.ok(3).filter((x) => x > 5, 'Value is too small')
// Err(Error: Value is too small)filterOrElse() — With Custom Error Type
Filter with full control over the error type returned on rejection:
type ValidationError = { field: string; message: string }
Result.ok(-5).filterOrElse(
(x) => x > 0,
(x): ValidationError => ({
field: 'age',
message: `Age must be positive, got ${x}`
})
)
// Err({ field: 'age', message: 'Age must be positive, got -5' })filter() vs filterOrElse()
filter()— error is alwaysError, accepts an optional message stringfilterOrElse()— full control over the error type via a factory function
// filter: error type is always Error
Result.ok(3).filter((x) => x > 5, 'Too small')
// Err(Error: 'Too small')
// filterOrElse: error type is whatever you return
Result.ok(3).filterOrElse(
(x) => x > 5,
(x) => ({ code: 'TOO_SMALL', value: x })
)
// Err({ code: 'TOO_SMALL', value: 3 })Flattening Nested Results
flatten()
Unwraps a Result<Result<T, E2>, E> into Result<T, E | E2>:
// Nested Ok
Result.ok(Result.ok(42)).flatten()
// Ok(42)
// Nested Err
Result.ok(Result.err('inner error')).flatten()
// Err('inner error')
// Outer Err is preserved
Result.err('outer error').flatten()
// Err('outer error')Use flatten() when a function that returns a Result calls another function that also returns a Result, and you end up with a nested Result:
function parseNumber(s: string): Result<number, string> {
const n = Number(s)
return isNaN(n) ? Result.err('not a number') : Result.ok(n)
}
function validatePositive(n: number): Result<number, string> {
return n > 0 ? Result.ok(n) : Result.err('must be positive')
}
// Without flatten — nested Result
const nested: Result<Result<number, string>, string> = Result.ok('5').map(validatePositive)
// With flatten — clean Result
const flat: Result<number, string> = Result.ok('5').map(validatePositive).flatten()
// Prefer andThen() for this pattern — it flattens automatically
const same: Result<number, string> = Result.ok('5').andThen(validatePositive)TIP
flatten() is useful for one-off cases. For sequential operations, prefer andThen() — it flattens automatically.
Combining Two Results
zip() — Into a Tuple
Combines two Results into a single Result containing a tuple of their values:
Result.ok(1).zip(Result.ok('a'))
// Ok([1, 'a'])
Result.ok(1).zip(Result.err('fail'))
// Err('fail')zipWith() — With a Combiner Function
Like zip(), but applies a function to the two values instead of producing a tuple:
Result.ok(2).zipWith(Result.ok(3), (a, b) => a + b)
// Ok(5)
Result.ok('hello').zipWith(Result.ok('world'), (a, b) => `${a} ${b}`)
// Ok('hello world')
Result.err('fail').zipWith(Result.ok(3), (a, b) => a + b)
// Err('fail')and() — Sequence Without Values
Returns the second Result if the first is Ok, ignoring the first value:
hasPermission()
.and(isAuthenticated())
.and(isVerified())Real-World Pipeline
User registration with validation:
async function registerUser(formData: FormData): AsyncResult<User, AppError> {
return Result.ok(formData)
// Validate
.andThen((data) => validateForm(data))
// Check if user exists
.andThenAsync(async (data) => {
const exists = await userExists(data.email)
return exists
? Result.err({ type: 'user_exists' })
: Result.ok(data)
})
// Hash password
.andThenAsync(async (data) => {
const hash = await hashPassword(data.password)
return Result.ok({ ...data, password: hash })
})
// Save to database
.andThenAsync(async (data) => {
const user = await db.createUser(data)
return user ? Result.ok(user) : Result.err({ type: 'db_error' })
})
}Mixing map() and andThen()
Result.ok(userId)
.map(id => fetchUserSync(id)) // Transform value
.andThen(validate) // Chain Result
.map(user => user.name) // Transform again
.unwrapOr('Anonymous') // FallbackCommon Mistakes
Forgetting to Return Result in andThen()
// ✗ Wrong: andThen expects a Result return
result.andThen(x => x * 2)
// ✓ Correct: return a Result
result.andThen(x => Result.ok(x * 2))
// ✓ Or use map() instead
result.map(x => x * 2)Deep Nesting
// ✗ Avoid: nested Results
Result.ok(Result.ok(42))
// ✓ Better: use andThen() to flatten automatically
Result.ok(5).andThen(x => Result.ok(x * 2))Best Practices
- Start with
Result.ok()when beginning a chain - Use
andThen()for sequential operations returning Result - Use
map()for simple transformations - Use
filterOrElse()when you need a typed error on rejection - Use
flatten()only when you cannot useandThen() - Use
orElse()for error recovery - Use
match()at the end for final handling - Use
inspect()andinspectErr()for debugging
Next Steps
- Pattern Matching — Handle final results
- Error Handling — Error recovery
- Async Operations — Work with Promises