[PROPOSAL] Do + bind / let for Result and ResultAsync
Author: iwasa-kosuiCreated Jun 22, 2026Updated Jun 25, 2026
Motivation
Chaining multiple dependent operations with andThen forces nested closures to keep earlier values in scope:
getUser(id)
.andThen(user =>
getRole(user).andThen(role =>
getPermissions(role).map(perms => ({ user, role, perms }))
)
)safeTry solves this with generators, but there's demand for a method-chain approach too.
fp-ts, Effect, and byethrow all have Do + bind for this. Same idea as Haskell's do notation, Scala's for comprehension, F#'s Computation Expressions, and Kotlin/Arrow's either { } + .bind().
Proposed API
Given the following functions:
declare function getUser(id: string): Result<User, GetUserError>
declare function getRole(user: User): Result<Role, GetRoleError>
declare function getPermissions(role: Role): Result<Perms, GetPermsError>Entry point
Result.Do // Result<{}, never>
ResultAsync.Do // ResultAsync<{}, never>bind: add a value from a Result-returning function
Result.Do
.bind("user", () => getUser(id))
.bind("role", ({ user }) => getRole(user))
.bind("perms", ({ role }) => getPermissions(role))
.map(({ user, role, perms }) => /* ... */)The callback receives the accumulated object and returns a Result (or ResultAsync). Short-circuits on Err.
let: add a pure value
Result.Do
.bind("user", () => getUser(id))
.let("greeting", ({ user }) => `Hello, ${user.name}`)
.map(({ user, greeting }) => /* ... */)For computed values that don't need Result wrapping.
ResultAsync example
Given:
declare function getUser(id: string): ResultAsync<User, GetUserError>
declare function getRole(user: User): ResultAsync<Role, GetRoleError>ResultAsync.Do
.bind("user", () => getUser(id))
.bind("role", ({ user }) => getRole(user))
.let("label", ({ user, role }) => `${user.name} (${role.name})`)
.map(({ user, role, label }) => ({ user, role, label }))Design notes
- Added as instance methods on
Ok/Err/ResultAsync, same asandTee/andThrough/orTee - Accumulator type grows via
{ [K in N | keyof A]: ... }on eachbind/let - Error types accumulate as a union (
E1 | E2 | ...), same asandThen - Duplicate keys are rejected at the type level with
Exclude<N, keyof A>
Related
- #186:
*CollectAPI request (closed). Same underlying problem - #641: Generator-based DSL proposal. Different approach, complementary
Source: supermacro/neverthrow