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.JavaScriptBind
Use Bind.error and Bind.mapError to give different bind sources the same error-assignment syntax.
Why use Bind
Without Bind, you must adapt the right-hand side before flow { } can bind it. The required transformation depends
on that source's shape. For example, mapping an Async<Result<_,_>> error requires another async { } block:
let authorizeForLogin user =
async {
let! result = authorize user
return result |> Result.mapError Unauthorized
}
let login user =
flow {
do! authorizeForLogin user
return user
}The equivalent transformation for a Result uses Result.mapError directly. A Flow uses Flow.mapError. Assigning
an error to Option or Async<Option<_>> requires another shape-specific conversion.
Bind gives these supported bind sources one bind-site syntax. You choose whether to assign or map the error; the
Flow computation expression handles the source shape:
flow {
let! profile = maybeProfile |> Bind.error ProfileNotFound
do! authorize user |> Bind.mapError Unauthorized
return! createToken user |> Bind.mapError TokenFailed
}Here, maybeProfile can be an Option or an asynchronous option source. Similarly, error mapping has the same form
for Result, Async<Result<_,_>>, and Flow.
Use Bind directly with let!, do!, or return!. It marks the error adaptation for that bind and does not run the
source. If the source already uses the workflow's error type, bind it without Bind.
Assign an error
Bind.error failure source assigns failure to a source that has no error value of its own. It preserves a present
or successful value. It turns None, ValueNone, or Error () into the supplied workflow error when flow { }
binds the source.
type User = { Name: string }
type LoginError = UserNotFound | InvalidPassword
let tryGetUser username : Async<User option> =
async { return if username = "ada" then Some { Name = username } else None }
let checkPassword password =
if System.String.IsNullOrWhiteSpace password then Error () else Ok ()
let login username password =
flow {
let! user =
tryGetUser username
|> Bind.error UserNotFound
do!
checkPassword password
|> Bind.error InvalidPassword
return user
}
04-error-handling_01-bind.md_page.UserName: stringstringAn abbreviation for the CLI type . Basic Types
04-error-handling_01-bind.md_page.LoginErrorUserNotFoundInvalidPasswordtryGetUser: string -> Async<User option>username: stringMicrosoft.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
optionThe type of optional values. When used from other CLI languages the empty option is the null value. Use the constructors Some and None to create values of this type. Use the values in the Option module to manipulate values of this type, or pattern match against the values directly. 'None' values will appear as the value null to other CLI languages. Instance methods on this type will appear as static methods to other CLI languages due to the use of null as a value representation. Options
async: AsyncBuilderBuilds an asynchronous workflow using computation expression syntax. let sleepExample() = async { printfn "sleeping" do! Async.Sleep 10 printfn "waking up" return 6 } sleepExample() |> Async.RunSynchronously
(=): '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
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
NoneThe representation of "No value"
checkPassword: string -> Result<unit,unit>password: stringSystemIsNullOrWhiteSpace: string -> boolIndicates whether a specified string is , empty, or consists only of white-space characters. The string to test. if the parameter is or , or if consists exclusively of white-space characters.
System.StringRepresents text as a sequence of UTF-16 code units.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
login: string -> string -> Flow<'a,LoginError,User>flow: FlowBuilderThe universal flow { } computation expression.
user: User(|>): '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.BindModuleCreates error-adaptation markers for let!, do!, and return! in flow { }.
error: 'error -> ^source -> BindError<'env,'error,'value>Assigns an error to a missing or unit-error source at a flow { } bind site. The error to use if the source fails. The source to adapt. A marker to use directly with let!, do!, or return! in flow { }. flow { let! user = maybeUser |> Bind.error InvalidUser do! Result.requireTrue () isValid |> Bind.error InvalidInput }
Bind.error accepts these source types:
Option<'value>ValueOption<'value>Result<'value, unit>Flow<'env, unit, 'value>Async<Option<'value>>,Task<Option<'value>>, andValueTask<Option<'value>>Async<ValueOption<'value>>,Task<ValueOption<'value>>, andValueTask<ValueOption<'value>>Async<Result<'value, unit>>,Task<Result<'value, unit>>, andValueTask<Result<'value, unit>>
For a Boolean condition, first return a Result that states what failure looks like. Then bind that result directly
if it already uses the workflow error type, or apply Bind.error if it uses unit.
Map an error
Use Bind.mapError when the source has a meaningful error that must be translated to the surrounding workflow's
error type.
type AuthError = Denied of string
type TokenError = Expired of string
type LoginError = Unauthorized of AuthError | TokenFailed of TokenError
let authorize user : Async<Result<unit, AuthError>> =
async { return Error (Denied user) }
let createToken user : Result<string, TokenError> =
Error (Expired user)
let login user =
flow {
do!
authorize user
|> Bind.mapError Unauthorized
return!
createToken user
|> Bind.mapError TokenFailed
}Bind.mapError accepts these source types:
Result<'value, 'error>Flow<'env, 'error, 'value>Async<Result<'value, 'error>>Task<Result<'value, 'error>>ValueTask<Result<'value, 'error>>
Use a marker only at a bind site
The value returned by Bind.error or Bind.mapError is a marker for the Flow computation expression. It is not a
general-purpose Result or Flow transformation. Keep it directly on the right side of let!, do!, or return!:
flow {
let! user = maybeUser |> Bind.error UserNotFound
return! createToken user |> Bind.mapError TokenFailed
}Outside flow { }, use functions for the source type, such as Result.mapError, Option.toResult, or
Flow.mapError.
Raw Task and ValueTask values still require an explicit
Task interop adapter. Bind changes an error at a computation-expression
bind; it does not change when an operation starts.
Choose between Bind and Policy
Use Bind for a one-time error adaptation at a specific bind site. Use a
Policy for a named, reusable verification rule that can read the workflow environment,
compose with other rules, or be enabled by the environment.

