Execution Semantics
This page shows the exact execution rules for Flow, including how a cold program becomes an Effect and resolves to an Exit.
FsFlow uses a unified Flow<'env, 'error, 'value> model that handles synchronous code, F# Async, and .NET Task interop natively.
Execution Shape
Conceptually, execution is:
Flow -> Effect -> Exit
More precisely:
Flowis the cold program you define.Effectis the deferred runnable carrier.Exitis the terminal outcome after execution.
Success and Typed Failure
Every Flow execution results in an Exit<'value, 'error>:
Exit.Success value: The happy path.Exit.Failure (Cause.Fail error): An expected domain-specific failure.Exit.Failure Cause.Interrupt: The workflow was signaled to stop (e.g., cancellation).Exit.Failure (Cause.Die exception): An unexpected defect or crash.
All standard combinators (map, bind, zip, orElse) are short-circuiting. The first Exit.Failure stops the workflow unless explicitly caught.
Short-Circuiting vs. Accumulated Validation
flow {}andresult {}: Ordered workflows. They stop on the first failure.validate {}: Accumulating validation. Joins errors from independent steps (usingand!) into a structuredDiagnosticsgraph.
Use the short-circuiting model when later steps depend on earlier values. Use the validation model when independent checks should all run and report back together.
Execution Is Explicit
You run a Flow by calling Flow.run.
Flow.run returns an Effect<'value, 'error>. The platform-specific carrier is defined by the target:
- On .NET:
Effect<'value, 'error>is aValueTask<Exit<'value, 'error>>. - On Fable:
Effect<'value, 'error>is anAsync<Exit<'value, 'error>>.
This design allows FsFlow to remain portable while respecting the execution models of different platforms. Effect is the cross-platform execution handle.
A flow is cold: building a flow does not run it. Each call to run executes the logic from scratch.
Why Exit Has Three Failure Causes
Exit.Failure carries a Cause<'error> instead of a plain error because not all failures mean the same thing.
Cause.Fail error: An expected domain failure. This is the normal typed error path.Cause.Interrupt: A cancellation or interruption signal. The runtime uses this when a workflow stops because it was asked to stop.Cause.Die exception: An unexpected defect or crash. This is for bugs, panic-like failures, and exceptions that were not intentionally translated into a typed error.
This split matters because one generic failure type cannot answer three different questions:
- Was this a business-rule failure that should participate in normal control flow?
- Was this a cancellation signal that should stop work immediately and usually not be retried?
- Was this a defect that should usually surface as a crash or be logged as an unexpected exception?
Why Not Just Use CancellationToken?
CancellationToken only models interruption. It does not model:
- a typed domain error like
Cause.Fail - a defect like
Cause.Die - the fact that a flow can fail for reasons other than cancellation
FsFlow still uses CancellationToken internally and for interoperability, but it converts cancellation into Cause.Interrupt at the edges where the runtime can observe it. That keeps cancellation as a first-class execution outcome instead of burying it inside the ambient .NET token API.
How Each Cause Gets Produced
Cause.Failis produced by explicit domain failures such asFlow.error,Flow.fail,Flow.fromResult, and validation branches that intentionally turn a rejected condition into a typed error.Cause.Interruptis produced when the runtime observes cancellation or interruption, such asFlow.interruptor a cancellation-aware helper likeFlow.Runtime.sleep.Cause.Dieis produced by defects that escape normal typed handling, such as unexpected exceptions. The runtime generally does not invent this value for you; it preserves the defect unless you intentionally catch and translate it.
Where The Runtime Helps
The runtime already does some of the translation work:
- cancellation-aware helpers convert
OperationCanceledExceptionintoCause.Interrupt - exception-catching helpers like
Flow.catchtranslate exceptions intoCause.Failwhen you want them treated as domain errors Exit.toResultre-raisesCause.DieandCause.Interruptwhen you collapse back into a plainResult, because those are not normal success-path errors
Interruption and Cancellation
FsFlow supports algebraic interruption. When a fiber is interrupted (e.g., via Flow.interrupt or a CancellationToken trigger), the flow stops executing and returns Exit.Failure Cause.Interrupt.
Environments
Flow reads dependencies explicitly:
Flow.env: Reads the whole environment.Flow.read: Projects one dependency.Flow.localEnv: Runs a smaller computation inside a larger environment.
Task Temperature
Flow distinguishes between:
- Hot Inputs: Already-started
Task<'value>orAsync<'value>. Re-running the flow re-awaits the same underlying work. - Cold Inputs: Logic defined inside
flow {}or helpers likeFlow.Runtime.sleep. Re-running the flow repeats the work.
Use Cold inputs when you want the effect to observe the runtime CancellationToken or repeat its side effects on retry.
Runtime Helpers
Operational helpers for logging, timeout, retry, and resource handling are grouped into the Flow.Runtime and Schedule modules.
Next
Read Getting Started for a tutorial-style overview, or browse the API Reference for detailed signatures.