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.JavaScriptDefects and Exceptions
Axial distinguishes expected failures, interruption, and unexpected defects. Domain failures stay in the typed error channel. Defects are recorded in the execution outcome so cleanup and observers can see them.
Quick Start: Usage Patterns
Producing Failures
Choose the function that matches your intent:
| Intent | Function | Outcome |
|---|---|---|
| Domain Error (Expected) | Flow.fail "Not found" |
Cause.Fail "Not found" |
| Defect/Panic (Bug) | Flow.die (exn "Database down") |
Cause.Die exn |
| Interruption | Fiber.interrupt or runtime cancellation |
Cause.Interrupt |
| Sequential Failures | Workflow fails, then cleanup fails | Cause.Then (workflowCause, cleanupCause) |
| Parallel Failures | Parallel branches both fail | Cause.Both (leftCause, rightCause) |
Bridging Exceptions
Use Flow.attemptAsync, Flow.attemptTask, or Flow.attemptValueTask when exceptions from an interop boundary are expected and should enter the typed error channel. These constructors return Cause.Fail exn for non-cancellation exceptions and Cause.Interrupt for cancellation.
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 loadConfig : ExnFlow<string> =
Flow.attemptTask (fun token -> File.ReadAllTextAsync("appsettings.json", token))
loadConfig: ExnFlow<string>ExnFlowA flow that requires no environment and uses exceptions as recoverable typed errors.
stringAn abbreviation for the CLI type . Basic Types
Axial.FlowattemptTask: (CancellationToken -> Task<'value>) -> Flow<'env,exn,'value>Creates a flow from a cancellable task factory and treats thrown exceptions as recoverable typed errors. Successful completion returns Exit.Success. OperationCanceledException returns Cause.Interrupt when the runtime token requested it. Other exceptions, including cancellation the operation raised for its own reasons, return Cause.Fail exn. Starts the operation, observing the supplied cancellation token. .NET only
token: CancellationTokenSystem.IO.FileProvides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of objects.
ReadAllTextAsync: string * CancellationToken -> Task<string>Asynchronously opens a text file, reads all the text in the file, and then closes the file. The file to open for reading. The token to monitor for cancellation requests. The default value is . A task that represents the asynchronous read operation, which wraps the string containing all text in the file.
Use Flow.catch to convert simple defects into domain errors after a flow has already produced Cause.Die. Existing typed failures and interruptions are preserved. Compound causes such as Cause.Then and Cause.Both are left unchanged.
type ParseError = InvalidNumber of string
let parseCount (text: string) : Flow<ParseError, int> =
Flow.delay (fun () -> Flow.ok (Int32.Parse text))
|> Flow.catch (function
| :? FormatException -> InvalidNumber text
| error -> raise error)
FsLiveDocsGeneratedPage14_4556FC416C2B.ParseErrorInvalidNumberstringAn abbreviation for the CLI type . Basic Types
parseCount: string -> Flow<ParseError,int>text: stringFlowA flow that requires no environment and can fail with a typed error.
intAn abbreviation for the CLI type . Basic Types
Axial.Flowdelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
ok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
System.Int32Represents a 32-bit signed integer.
Parse: string -> intConverts the string representation of a number to its 32-bit signed integer equivalent. A string containing a number to convert. A 32-bit signed integer equivalent to the number contained in . is . is not in the correct format. represents a number less than or greater than .
(|>): '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
catch: (exn -> 'error) -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Catches exceptions raised during execution and simple defect outcomes, then maps them to a typed error. Thrown exceptions and simple Cause.Die outcomes are converted to Cause.Fail. Existing typed failures and interruptions are preserved, and an OperationCanceledException thrown because the runtime's token was cancelled stays an interruption. Compound causes are preserved unchanged. A function of type exn -> 'error to map the exception. The source flow of type to monitor. A that converts recoverable exceptions into typed errors. let flow = Flow.die (System.Exception("boom")) |> Flow.catch (fun ex -> "caught: " + ex.Message)
System.FormatExceptionThe exception that is thrown when the format of an argument is invalid, or when a composite format string is not well formed.
error: exnraise: Exception -> 'TRaises an exception The exception to raise. The result value. open System.IO exception FileNotFoundException of string let readFile (fileName: string) = if not (File.Exists(fileName)) then raise(FileNotFoundException(fileName)) File.ReadAllText(fileName) readFile "/this-file-doest-exist" When executed, raises a FileNotFoundException.
Int32.Parse throws, so the flow first ends with Cause.Die; Flow.catch then turns a FormatException into the
typed error:
parseCount "42" |> Flow.run () |> shouldEqual (Exit.Success 42)
parseCount "forty" |> Flow.run () |> shouldEqual (Exit.Failure(Cause.Fail(InvalidNumber "forty")))
parseCount: string -> Flow<ParseError,int>(|>): '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.
InvalidNumberRationale
Axial records defects in the Exit type for three reasons.
1. One Outcome Shape
In complex orchestration like Flow.zipPar (running two flows concurrently), the engine must coordinate the lifecycle of multiple fibers.
- Problem: If a defect is only a thrown exception, it escapes the return value. The engine has to handle two failure paths: returned failures and thrown exceptions.
- Approach: By capturing defects into the
Exittype, every flow execution returns a value. If one branch dies, the engine receives it as data, can interrupt the other branches, and returns one structured outcome.
2. Concurrency Coordination
When a fiber fails, you often need to perform cleanup (e.g., ensuring or onExit).
By recording defects as Cause.Die, Axial passes the original exception and stack trace to finalizers as a value. Finalizers can log why a background fiber died without adding try...with blocks around every cleanup action.
If cleanup itself fails after the workflow has already failed, Axial does not discard either side. It returns Cause.Then (workflowCause, cleanupCause) so observability and host boundaries can see the original failure and the cleanup defect in order.
3. Precision in Retries and Fallbacks
The distinction between Fail and Die gives retry and fallback code a clear default:
- Retries should usually target
Fail(e.g., a transient network error), but neverDie(e.g., aNullReferenceException). Retrying a bug is usually a waste of resources. - Fallbacks (
orElse) usually target domain failures. If a workflow has a defect, it usually indicates a corrupted state that fallback logic wasn't designed to handle.

