Skip to content

Async Operations

Working with Promises and async/await in Result chains.

AsyncResult Type

A type alias for a Promise that resolves to a Result:

typescript
type AsyncResult<T, E> = Promise<Result<T, E>>

Creating Async Results

fromPromise()

Wrap Promises that might reject:

typescript
const user = await Result.fromPromise(
  () => fetch('/api/user').then(r => r.json())
)

if (user.isOk()) {
  console.log(user.unwrap())
}

With custom error handling:

typescript
type NetworkError = { type: 'network'; status?: number }

const data = await Result.fromPromise(
  () => fetch('/api/data').then(r => r.json()),
  (err): NetworkError => ({
    type: 'network',
    status: err instanceof Response ? err.status : undefined
  })
)

With async function:

typescript
const result = await Result.fromPromise(async () => {
  const response = await fetch('/api/data')
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
})

Async Transformations

mapAsync()

Transform values asynchronously:

typescript
await Result.ok(userId)
  .mapAsync(async (id) => await fetchUser(id))

Auto-flattens if the mapper returns a Result:

typescript
await Result.ok(input)
  .mapAsync(async (x) => {
    const result = await validate(x)
    return result ? Result.ok(x) : Result.err('invalid')
  })

mapErrAsync()

Transform errors asynchronously:

typescript
await result.mapErrAsync(async (error) => ({
  ...error,
  context: await fetchContext(),
  timestamp: Date.now()
}))

mapOrAsync()

Transform with fallback:

typescript
const value = await Result.ok(5).mapOrAsync(
  async (x) => await expensiveComputation(x),
  defaultValue
)

mapOrElseAsync()

Transform using appropriate async mapper for each case:

typescript
const value = await result.mapOrElseAsync(
  async (x) => x * 2,
  async (e) => -1
)

Async Chaining

andThenAsync()

Chain async operations that return Results:

typescript
const result = await Result.ok(userId)
  .andThenAsync(async (id) => {
    const user = await fetchUser(id)
    return user ? Result.ok(user) : Result.err('not found')
  })

orElseAsync()

Async error recovery:

typescript
const result = await fetchFromPrimary()
  .orElseAsync(async () => await fetchFromCache())
  .orElseAsync(async () => await fetchFromAPI())

andAsync() / orAsync()

Work with Promises directly:

typescript
const nextOp: AsyncResult<number, string> = Promise.resolve(Result.ok(42))

await result.andAsync(nextOp)  // If Ok, return Promise
await result.orAsync(nextOp)   // If Err, return Promise

Parallel Operations

Multiple Independent Operations

typescript
async function loadDashboard(userId: string) {
  const results = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId),
    fetchNotifications(userId)
  ])

  return Result.all(results).map(([user, posts, notifs]) => ({
    user, posts, notifs
  }))
}

Collect All Without Failing

typescript
const results = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchComments(id)
])

const settled = Result.allSettled(results).unwrap()

settled.forEach((result) => {
  if (result.status === 'ok') {
    process(result.value)
  } else {
    logError(result.reason)
  }
})

Real-World Pipeline

Complex async workflow:

typescript
async function processUserRegistration(
  formData: FormData
): AsyncResult<User, RegistrationError> {
  return Result.ok(formData)
    // Validate locally
    .andThen((data) => validateForm(data))

    // Check if user exists (async)
    .andThenAsync(async (data) => {
      const exists = await checkUserExists(data.email)
      return exists
        ? Result.err({ type: 'user_exists' })
        : Result.ok(data)
    })

    // Hash password (async)
    .andThenAsync(async (data) => {
      const hash = await bcrypt.hash(data.password)
      return Result.ok({ ...data, password: hash })
    })

    // Create user in database (async)
    .andThenAsync(async (data) => {
      const user = await db.users.create(data)
      return user ? Result.ok(user) : Result.err({ type: 'db_error' })
    })
}

Retry with Exponential Backoff

typescript
async function fetchWithRetry<T>(
  url: string,
  maxRetries: number = 3
): AsyncResult<T, Error> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const result = await Result.fromPromise(
      () => fetch(url).then(r => r.json())
    )

    if (result.isOk()) return result

    if (attempt < maxRetries - 1) {
      const delay = Math.pow(2, attempt) * 1000
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }

  return Result.err(new Error('Max retries exceeded'))
}

Error Handling with Abort Signals

typescript
async function fetchWithTimeout(
  url: string,
  timeout: number = 5000
): AsyncResult<unknown, Error> {
  const controller = new AbortController()
  const id = setTimeout(() => controller.abort(), timeout)

  const result = await Result.fromPromise(
    () => fetch(url, { signal: controller.signal }).then(r => r.json()),
    () => new Error('Request timeout')
  )

  clearTimeout(id)
  return result
}

Mixing Sync and Async

typescript
const result = await Result.ok(data)
  .map(parseJSON)                          // Sync
  .andThenAsync(async (d) => {            // Async
    const valid = await validate(d)
    return valid ? Result.ok(d) : Result.err('invalid')
  })
  .mapErr(enrichError)                    // Sync
  .orElseAsync(async () => {             // Async
    return Result.ok(await fetchDefault())
  })

Common Pitfalls

Forgetting await

typescript
// ✗ Wrong: result is Promise<Result>, not Result
const result = Result.ok(1).mapAsync(async x => x * 2)
console.log(result.unwrap()) // Runtime error

// ✓ Correct: await to get Result
const result = await Result.ok(1).mapAsync(async x => x * 2)
console.log(result.unwrap()) // 2

Not Returning Result in andThenAsync()

typescript
// ✗ Wrong: andThenAsync expects a Result return
.andThenAsync(async x => x * 2)

// ✓ Correct: return a Result
.andThenAsync(async x => Result.ok(x * 2))

Best Practices

  1. Always await async Result methods
  2. Use fromPromise() to wrap Promises
  3. Chain with andThenAsync() for sequential async operations
  4. Use Promise.all() + Result.all() for parallel operations
  5. Add timeouts for network requests
  6. Retry with backoff for flaky operations
  7. Use inspectErr() for logging side effects

Next Steps