Skip to content

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:

typescript
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 skipped

Core Methods

map() — Transform Success Values

Transform without returning a Result:

typescript
Result.ok(5)
  .map(x => x * 2)        // Ok(10)
  .map(x => x + 5)        // Ok(15)
  .unwrap()               // 15

andThen() — Chain Results

Use when each operation returns a Result:

typescript
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)                 // 0

orElse() — Recover from Error

Execute recovery when an error occurs:

typescript
fetchFromCache(key)
  .orElse(() => fetchFromDatabase(key))
  .orElse(() => fetchFromAPI(key))
  .unwrapOr(defaultData)

Conditional recovery:

typescript
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:

typescript
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:

typescript
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 always Error, accepts an optional message string
  • filterOrElse() — full control over the error type via a factory function
typescript
// 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>:

typescript
// 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:

typescript
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:

typescript
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:

typescript
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:

typescript
hasPermission()
  .and(isAuthenticated())
  .and(isVerified())

Real-World Pipeline

User registration with validation:

typescript
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()

typescript
Result.ok(userId)
  .map(id => fetchUserSync(id))     // Transform value
  .andThen(validate)                // Chain Result
  .map(user => user.name)           // Transform again
  .unwrapOr('Anonymous')            // Fallback

Common Mistakes

Forgetting to Return Result in andThen()

typescript
// ✗ 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

typescript
// ✗ 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

  1. Start with Result.ok() when beginning a chain
  2. Use andThen() for sequential operations returning Result
  3. Use map() for simple transformations
  4. Use filterOrElse() when you need a typed error on rejection
  5. Use flatten() only when you cannot use andThen()
  6. Use orElse() for error recovery
  7. Use match() at the end for final handling
  8. Use inspect() and inspectErr() for debugging

Next Steps