Taming Nested Results in F# with Computation Expressions
Stop nesting Result.bind calls in F#. Learn how to use Computation Expressions to create a clean, linear validation pipeline that short-circuits on the first error.
15 Sept 2026, 22:59 UTC

The "Pyramid of Doom" in Result Pipelines
When building a domain workflow—such as validating a payment request—you often face a sequence of operations where each step depends on the success of the previous one. In F#, the standard way to handle this is using the Result<'T, 'E> type. However, chaining these operations using Result.bind or pattern matching often leads to deeply indented code or a verbose chain of pipes that becomes difficult to read as the business logic grows.
The goal is to write code that looks like a linear sequence of successful steps, but automatically handles the failure case (short-circuiting) without requiring explicit checks at every line.
Defining a Result Builder
Computation Expressions (CEs) allow you to define custom control flow. By creating a ResultBuilder, you can transform a block of code into a series of Result.bind calls. This effectively creates a domain-specific language (DSL) for your validation logic.
To implement a basic builder, you must define a type that implements Return (to wrap a value in Ok) and Bind (to chain the next operation if the current one is Ok).
type ResultBuilder() =
member _.Return(value) = Ok value
member _.Bind(monad, f) = Result.bind f monad
let result = ResultBuilder()
Worked Example: Payment Validation Workflow
Consider a scenario where we must validate a payment request: we need to ensure the amount is positive, the currency is supported, and the account is active. If any step fails, the entire process should stop and return the specific error.
// Domain Types
type PaymentRequest = { Amount: decimal; Currency: string; AccountId: int }
type PaymentError =
| InvalidAmount
| UnsupportedCurrency
| AccountInactive
// Validation functions
let validateAmount req =
if req.Amount > 0m then Ok req else Error InvalidAmount
let validateCurrency req =
if req.Currency = "USD" then Ok req else Error UnsupportedCurrency
let checkAccountStatus req =
// Simulate a database check
if req.AccountId <> 0 then Ok req else Error AccountInactive
// The Workflow using the builder
let processPayment request = result {
let !validAmount = validateAmount request
let !validCurrency = validateCurrency !validAmount
let !finalRequest = checkAccountStatus !validCurrency
!finalRequest
}
How to run and verify this
- Open F# Interactive (fsi) or a .fsx script file.
- Paste the
ResultBuilderand the domain logic above. - Test with a failing case:
processPayment { Amount = -10m; Currency = "USD"; AccountId = 1 }. - Expected Result:
Error InvalidAmount. - Test with a successful case:
processPayment { Amount = 100m; Currency = "USD"; AccountId = 1 }. - Expected Result:
Ok { Amount = 100m; ... }.
Trade-offs and Limitations
While computation expressions clean up the syntax, they introduce specific trade-offs:
- Cognitive Load: Developers unfamiliar with the "monadic" nature of CEs may find the
!(bang) syntax or the hiddenBindcalls confusing compared to explicit pattern matching. - Stack Traces: Because the compiler desugars the
result { ... }block into a series of function calls, stack traces can become slightly more opaque during debugging. - Overhead: CEs are syntactic sugar. While the performance hit is negligible for most business applications, they do involve more function allocations than a simple
if/elseblock.
Practical Verification
To ensure your builder is behaving correctly, compare the output of the result { ... } block against a manual chain of Result.bind calls. If the outputs are identical for all edge cases (especially the first failure in a sequence), the builder is correctly implementing the short-circuiting logic.
For larger projects, consider using established libraries like FsToolkit.ErrorHandling, which provides a pre-built result { ... } builder and additional utilities for combining multiple errors rather than just returning the first one.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.