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

Bind

Bind.error and Bind.mapError assign or map errors at a flow { } bind site, regardless of the source type.

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:

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 AuthError = Denied of string
type TokenError = Expired of string

type SessionError =
    | Unauthorized of AuthError
    | TokenFailed of TokenError
    | ProfileNotFound

let authorize (user: string) : Async<Result<unit, AuthError>> =
    async { return if user = "ada" then Ok() else Error(Denied user) }

let createToken (user: string) : Result<string, TokenError> =
    if user = "ada" then Ok $"token-{user}" else Error(Expired user)

let authorizeForLogin (user: string) : Async<Result<unit, SessionError>> =
    async {
        let! result = authorize user
        return result |> Result.mapError Unauthorized
    }

let signIn (user: string) : Flow<SessionError, string> =
    flow {
        do! authorizeForLogin user
        return user
    }
Requires & types
Requires: open Axial (packages: Axial)
val authorize: string -> Async<Result<unit,AuthError>>
val createToken: string -> Result<string,TokenError>
val authorizeForLogin: string -> Async<Result<unit,SessionError>>
val signIn: string -> Flow<SessionError,string>

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.

With Bind, you assign or map the error at the bind site; the Flow computation expression handles the source shape:

Verified
let startSession (maybeProfile: string option) : Flow<SessionError, string> =
    flow {
        let! profile = maybeProfile |> Bind.error ProfileNotFound
        do! authorize profile |> Bind.mapError Unauthorized
        return! createToken profile |> Bind.mapError TokenFailed
    }
Requires & types
Requires: open Axial (packages: Axial)
val startSession: string option -> Flow<SessionError,string>
Verified
startSession (Some "ada") |> Flow.run () |> shouldEqual (Exit.Success "token-ada")
startSession None |> Flow.run () |> shouldEqual (Exit.Failure(Cause.Fail ProfileNotFound))
startSession (Some "bob") |> Flow.run () |> shouldEqual (Exit.Failure(Cause.Fail(Unauthorized(Denied "bob"))))
Requires & types
Requires: open Axial (packages: Axial)

maybeProfile can be an Option or an asynchronous option source. 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.

Verified
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
    }
Requires & types
Requires: open Axial (packages: Axial)
val tryGetUser: string -> Async<User option>
val checkPassword: string -> Result<unit,unit>
val login: string -> string -> Flow<'a,LoginError,User>
Verified
login "ada" "secret" |> Flow.run () |> shouldEqual (Exit.Success { Name = "ada" })
login "bob" "secret" |> Flow.run () |> shouldEqual (Exit.Failure(Cause.Fail UserNotFound))
login "ada" " " |> Flow.run () |> shouldEqual (Exit.Failure(Cause.Fail InvalidPassword))
Requires & types
Requires: open Axial (packages: Axial)

Bind.error accepts these source types:

  • Option<'value>
  • ValueOption<'value>
  • Result<'value, unit>
  • Flow<'env, unit, 'value>
  • Async<Option<'value>>, Task<Option<'value>>, and ValueTask<Option<'value>>
  • Async<ValueOption<'value>>, Task<ValueOption<'value>>, and ValueTask<ValueOption<'value>>
  • Async<Result<'value, unit>>, Task<Result<'value, unit>>, and ValueTask<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.

With authorize and createToken from the start of this page:

Verified
let issueToken (user: string) : Flow<SessionError, string> =
    flow {
        do!
            authorize user
            |> Bind.mapError Unauthorized

        return!
            createToken user
            |> Bind.mapError TokenFailed
    }
Requires & types
Requires: open Axial (packages: Axial)
val issueToken: string -> Flow<SessionError,string>
Verified
issueToken "ada" |> Flow.run () |> shouldEqual (Exit.Success "token-ada")
issueToken "bob" |> Flow.run () |> shouldEqual (Exit.Failure(Cause.Fail(Unauthorized(Denied "bob"))))
Requires & types
Requires: open Axial (packages: Axial)

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, so it only compiles directly on the right side of let!, do!, or return!:

Verified
let atBindSites (maybeUser: string option) : Flow<SessionError, string> =
    flow {
        let! user = maybeUser |> Bind.error ProfileNotFound
        return! createToken user |> Bind.mapError TokenFailed
    }
Requires & types
Requires: open Axial (packages: Axial)
val atBindSites: string option -> Flow<SessionError,string>

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.