Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScriptFsToolkit.ErrorHandling Comparison
FsToolkit.ErrorHandling and Axial both make expected failure explicit, but they operate at different levels.
FsToolkit.ErrorHandling provides computation expressions and combinators for types such as Result,
Async<Result<_, _>>, and Task<Result<_, _>>. It makes railway-oriented application code concise without adding a
runtime model.
Axial's Flow<'env, 'error, 'value> also has a typed error channel. In addition, it describes the environment a
workflow requires and gives execution a common model for interruption, resource lifetime, parallel composition,
retry, and defects.
The difference in one table
| Concern | FsToolkit.ErrorHandling | Axial |
|---|---|---|
| Expected failure | Result in the chosen carrier |
The 'error channel of Flow |
| Dependencies | Ordinary function parameters or closures | The 'env parameter, read with Flow.envWith |
| Execution carrier | Chosen up front: Result, AsyncResult, TaskResult, and related builders |
Flow describes the workflow; the runtime executes it |
| Cancellation | The underlying Async or Task code owns token propagation |
The runtime passes its cancellation token to cold work and represents cancellation as Cause.Interrupt |
| Defects | Usually faulted tasks or raised exceptions outside Result |
Bound work's exceptions become Cause.Die, so defects participate in Flow's concurrency semantics |
| Resource lifetime | Ordinary use, use!, try/finally, or application helpers |
use and use! in flow { } for lexical lifetimes; Flow.scoped and the Flow.scope... operations for wider ownership |
| Retry, timeout, and parallel policy | Application code or another library | Runtime combinators over the workflow |
Neither approach replaces domain validation. Both can carry a validation error type; accumulating independent errors still requires a validation abstraction rather than monadic short-circuiting.
taskResult and environment-free Flow
The closest Flow equivalent to a taskResult { } expression is Flow<'error, 'value>, an abbreviation for
Flow<unit, 'error, 'value>. Both forms short-circuit on an expected error and require no environment. Here is the
same operation in each computation expression.
FsToolkit.ErrorHandling
Shared setup
// Setup for the checked examples on this page.
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actual
SystemIOThreadingTasksAxialLayersConsoleFileSystemHostingBrowserNodePlatformServiceStateTelemetryJavaScriptshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
let loadCustomer repository customerId : Task<Result<Customer * Address, CustomerError>> =
taskResult {
let! customer = repository.Find customerId
let! address = repository.LoadAddress customer.AddressId
return customer, address
}Axial
type Customer = { Id: int; AddressId: int }
type Address = { Street: string }
type CustomerError = CustomerNotFound of int
type CustomerRepository =
{ Find: int -> Task<Result<Customer, CustomerError>>
LoadAddress: int -> Task<Result<Address, CustomerError>> }
let loadCustomerFlow (repository: CustomerRepository) customerId : Flow<CustomerError, Customer * Address> =
flow {
let! customer = ColdTask(fun _ -> repository.Find customerId)
let! address = ColdTask(fun _ -> repository.LoadAddress customer.AddressId)
return customer, address
}
FsLiveDocsGeneratedPage5_4556FC416C2B.CustomerId: intintAn abbreviation for the CLI type . Basic Types
AddressId: intFsLiveDocsGeneratedPage5_4556FC416C2B.AddressStreet: stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage5_4556FC416C2B.CustomerErrorCustomerNotFoundFsLiveDocsGeneratedPage5_4556FC416C2B.CustomerRepositoryFind: int -> Task<Result<Customer,CustomerError>>System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
LoadAddress: int -> Task<Result<Address,CustomerError>>loadCustomerFlow: CustomerRepository -> int -> Flow<CustomerError,(Customer * Address)>repository: CustomerRepositorycustomerId: intFlowA flow that requires no environment and can fail with a typed error.
flow: FlowBuilderThe universal flow { } computation expression.
customer: CustomerColdTaskaddress: Addresslet repository =
{ Find = fun id -> Task.FromResult(if id = 1 then Ok { Id = 1; AddressId = 10 } else Error(CustomerNotFound id))
LoadAddress = fun _ -> Task.FromResult(Ok { Street = "1 Main St" }) }
loadCustomerFlow repository 1 |> Flow.run () |> shouldEqual (Exit.Success({ Id = 1; AddressId = 10 }, { Street = "1 Main St" }))
loadCustomerFlow repository 2 |> Flow.run () |> shouldEqual (Exit.Failure(Cause.Fail(CustomerNotFound 2)))
repository: CustomerRepositoryFind: int -> Task<Result<Customer,CustomerError>>id: intSystem.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
Id: intAddressId: intErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
CustomerNotFoundLoadAddress: int -> Task<Result<Address,CustomerError>>Street: stringloadCustomerFlow: CustomerRepository -> int -> Flow<CustomerError,(Customer * Address)>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
FailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
FailAn expected domain-specific failure.
A Task starts when the method that returns it runs, so the Flow version wraps each call in ColdTask: the call then
happens when the Flow runs, and every run makes it again.
The similar source hides an execution difference. Calling loadCustomer starts and returns a Task; calling
loadCustomerFlow returns a cold workflow description that starts when a Flow runtime runs it. The runtime passes its
cancellation token through cold Flow and ColdTask operations, so timeout, race, and parallel-composition operators
can interrupt participating work. An already-started Task, including the result of calling a taskResult function,
cannot receive that token after it has started; expose cancellation-aware task code as a token-taking factory or
ColdTask when it must participate.
Flow also classifies outcomes for concurrency. An Error remains an expected Cause.Fail, cancellation becomes
Cause.Interrupt, and an exception thrown by bound work becomes Cause.Die. Operators such as Flow.zipPar can then
interrupt a sibling after either an expected failure or defect, and preserve concurrent failures in the resulting
Cause, rather than flattening every non-success outcome into the task exception channel.
FsToolkit.ErrorHandling fits synchronous Result pipelines and small functions that compose Task<Result<_, _>> calls
without starting concurrent work or owning resources. Once a pipeline needs cancellation, a timeout, parallel work, or
cleanup, the environment-free Flow form handles them by the same rules as the rest of the application.
Prefer Flow for application orchestration
Use Flow when composition itself needs a contract: required services, managed resources, interruption, retry policy,
or a distinction between expected failures and defects. The flow { } computation expression supports use and
use! for resources owned by one lexical block. Flow.scoped and Flow.scopeAcquireRelease cover lifetimes that
need an explicit runtime ownership boundary or extend beyond that block.
type Receipt = { CustomerId: int; Reference: string }
type PaymentGateway =
{ Charge: Customer -> Async<Result<Receipt, CustomerError>> }
type CheckoutEnv =
{ Customers: CustomerRepository
Payments: PaymentGateway }
let checkout customerId : Flow<CheckoutEnv, CustomerError, Receipt> =
flow {
let! customers = Flow.envWith _.Customers
let! payments = Flow.envWith _.Payments
let! customer = ColdTask(fun _ -> customers.Find customerId)
let! receipt = payments.Charge customer
return receipt
}
FsLiveDocsGeneratedPage5_4556FC416C2B.ReceiptCustomerId: intintAn abbreviation for the CLI type . Basic Types
Reference: stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage5_4556FC416C2B.PaymentGatewayCharge: Customer -> Async<Result<Receipt,CustomerError>>FsLiveDocsGeneratedPage5_4556FC416C2B.CustomerMicrosoft.FSharp.Control.FSharpAsync`1An asynchronous computation, which, when run, will eventually produce a value of type T, or else raises an exception. This type has no members. Asynchronous computations are normally specified either by using an async expression or the static methods in the type. See also F# Language Guide - Async Workflows. Library functionality for asynchronous programming, events and agents. See also Asynchronous Programming, Events and Lazy Expressions in the F# Language Guide. Async Programming
Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
FsLiveDocsGeneratedPage5_4556FC416C2B.CustomerErrorFsLiveDocsGeneratedPage5_4556FC416C2B.CheckoutEnvCustomers: CustomerRepositoryFsLiveDocsGeneratedPage5_4556FC416C2B.CustomerRepositoryPayments: PaymentGatewaycheckout: int -> Flow<CheckoutEnv,CustomerError,Receipt>customerId: intAxial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
flow: FlowBuilderThe universal flow { } computation expression.
customers: CustomerRepositoryAxial.FlowenvWith: ('env -> 'value) -> Flow<'env,'error,'value>Projects one value from the current environment. This is the primary way to access app dependencies, configuration, or request metadata stored in env. The projection runs only when the flow is executed, so constructing the flow is still pure and side-effect free. Prefer small projections over passing a large environment deeper into reusable helpers. A function that extracts a value from the environment. A containing the projected value. let currentTime () = Flow.envWith (fun (environment: BaseRuntime) -> environment.Clock.UtcNow())
_arg1: CheckoutEnvpayments: PaymentGateway_arg3: CheckoutEnvcustomer: CustomerColdTaskFind: int -> Task<Result<Customer,CustomerError>>receipt: ReceiptHere the environment is part of the workflow type rather than a closure or a parameter repeated through each layer.
At the application boundary, Flow.run returns an Exit; expected errors, defects, and interruption remain distinct
instead of sharing the task's exception channel.
Use them together
Adoption does not require rewriting FsToolkit.ErrorHandling functions. The Flow computation expression binds
Result<'value, 'error> and Async<Result<'value, 'error>> directly, and Task<Result<'value, 'error>> through
ColdTask. All of them continue on Ok and short-circuit the Flow on Error.
For example, an existing eligibility check can remain an asyncResult function:
let verifyCustomer customer : Async<Result<unit, CustomerError>> =
asyncResult {
do! verifyEmail customer.Email
do! verifyAccount customer.Id
}A Flow can then bind the taskResult-based loadCustomer function through ColdTask and bind this asyncResult
function directly in the same block:
let prepareOrder customerId : Flow<OrderEnv, CustomerError, Order> =
flow {
let! repository = Flow.envWith _.Customers
let! customer, address =
ColdTask(fun _ ->
loadCustomer repository customerId)
do! verifyCustomer customer // Async<Result<_, _>>
return createOrder customer address
}Because flow { } binds all of these (tasks through ColdTask), leaf functions can keep returning Result,
Async<Result<_, _>>, or Task<Result<_, _>>, and FsToolkit.ErrorHandling can keep building them. Flow takes over where operations are
composed: where dependencies, cancellation, timeouts, retries, and parallel work come in.

