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.HttpClient
open Axial.PlatformService
open Axial.Process
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.acquireReleaseWith and scopes 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
let loadCustomer repository customerId : Task<Result<Customer * Address, CustomerError>> =
taskResult {
let! customer = repository.Find customerId
let! address = repository.LoadAddress customer.AddressId
return customer, address
}let loadCustomerFlow repository customerId : Flow<CustomerError, Customer * Address> =
flow {
let! customer = repository.Find customerId
let! address = repository.LoadAddress customer.AddressId
return customer, address
}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.
Prefer FsToolkit.ErrorHandling when Task<Result<_, _>> is the honest contract and no larger runtime model is needed.
It is often the smaller choice for leaf functions and request handlers. Prefer the environment-free Flow form when
that local pipeline also needs Flow's execution, resource, or composition semantics.
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.acquireReleaseWith and scopes cover lifetimes that need an
explicit acquisition boundary or extend beyond that block.
type CheckoutEnv =
{ Customers: CustomerRepository
Payments: PaymentGateway }
let checkout customerId : Flow<CheckoutEnv, CheckoutError, Receipt> =
flow {
let! customers = Flow.envWith _.Customers
let! payments = Flow.envWith _.Payments
let! customer = customers.Find customerId
let! receipt = payments.Charge customer
return receipt
}Use them together
Adoption does not require rewriting FsToolkit.ErrorHandling functions. The Flow computation expression binds its
common carriers directly: Result<'value, 'error>, Async<Result<'value, 'error>>, and
Task<Result<'value, 'error>> all 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
}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
}
