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.JavaScript

Error Handling

A Flow has a typed channel for failures the caller is expected to handle:

Verified
> type PaymentError = CardDeclined | AccountClosed;;
> let charge () : Flow<PaymentError, decimal> = Flow.fail CardDeclined;;
> Flow.run () (charge ());;
val it: Exit<decimal,PaymentError> = Failure (Fail CardDeclined)

Unexpected exceptions are defects. The execution outcome retains them outside the workflow's typed error channel:

Verified
> open System;;
> type PaymentError = CardDeclined | AccountClosed;;
> let broken () : Flow<PaymentError, decimal> = Flow.die (InvalidOperationException "broken invariant");;
> match Flow.run () (broken ()) with Exit.Failure (Cause.Die error) -> error.Message | _ -> "";;
val it: string = "broken invariant"

An Exit distinguishes the cases:

Cause Meaning
Cause.Fail error Expected typed failure
Cause.Die exception Unexpected defect
Cause.Interrupt Cooperative cancellation or interruption
Cause.Then (first, second) Sequentially combined causes
Cause.Both (left, right) Concurrently combined causes
Cause.Traced (cause, trace) Cause with diagnostic context

Use an attempt constructor only when an exception from an interop API is an expected outcome. Use Flow.catch only when the application deliberately translates a defect into a typed error.

Use Flow.recover when a typed failure needs a fallback with a different error type, Flow.orElseWith when it keeps the same error type, and Flow.fold to inspect every cause. See Recovery and compound causes for what happens when error types change and when a handler throws.

Show page setup
// Setup for the checked examples on this page.
open Axial
/// 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
Verified
type PaymentError = CardDeclined | AccountClosed

let declined : Flow<PaymentError, decimal> = Flow.fail CardDeclined
let broken : Flow<PaymentError, decimal> = Flow.die (InvalidOperationException "boom")
Requires & types
Requires: open Axial (packages: Axial)
val declined: Flow<PaymentError,decimal>
val broken: Flow<PaymentError,decimal>
Verified
// orElseWith recovers a typed failure and keeps the same error type.
declined
|> Flow.orElseWith (fun _ -> Flow.ok 0m)
|> Flow.run ()
|> shouldEqual (Exit.Success 0m : Exit<decimal, PaymentError>)

// recover also recovers a typed failure but may change the error type.
declined
|> Flow.recover (fun _ -> Flow.ok 0m)
|> Flow.run ()
|> shouldEqual (Exit.Success 0m : Exit<decimal, Never>)

// Neither handles a defect; fold sees every cause, including defects.
broken
|> Flow.orElseWith (fun _ -> Flow.ok 0m)
|> Flow.run ()
|> function
| Exit.Failure(Cause.Die error) -> shouldEqual "boom" error.Message
| other -> failwithf "expected a defect, got %A" other

broken
|> Flow.fold (fun value -> Flow.ok $"value {value}") (fun cause -> Flow.ok (Cause.prettyPrint string cause))
|> Flow.run ()
|> shouldEqual (Exit.Success "Die(InvalidOperationException: boom)")
Requires & types
Requires: open Axial (packages: Axial)

Exit.toResult loses information. Use it only at a boundary that has decided how to represent defects, interruption, and combined causes.

Learn more