Errors
An Effect<'r, 'a, 'e> either succeeds with an 'a or fails with an 'e. The error is part of the type, so the
ways an effect can fail are visible in its signature, and the compiler checks that they are handled. Exceptions are
left for what is truly unexpected: a bug, or a failure nothing sensible can be done about.
This page shows how to model errors, how to combine effects that fail in different ways, how to handle errors, how
to collect several errors at once, and where exceptions fit in. It is a script: it runs with
dotnet fsi docs/errors.fsx once Orsak is built, and the output shown is from such a run.
Errors as a union
The examples place orders in a shop, which needs a stock service and a payment service. Each is described by an interface in the usual way, and each fails in its own way, described by a union:
open System
open System.Threading.Tasks
open Orsak
type StockError =
| OutOfStock of sku: string
| UnknownProduct of sku: string
type IStock =
abstract member Reserve: sku: string * quantity: int -> Task<Result<unit, StockError>>
type IStockProvider =
abstract member Stock: IStock
type PaymentError =
| Declined of reason: string
| ProviderUnavailable
type IPayments =
abstract member Charge: customer: string * amount: decimal -> Task<Result<string, PaymentError>>
type IPaymentsProvider =
abstract member Payments: IPayments
When the function given to Effect.Create returns a Result, its error becomes the effect's error:
module Stock =
let reserve sku quantity =
Effect.Create(fun (p: #IStockProvider) -> p.Stock.Reserve(sku, quantity))
module Payments =
let charge customer amount =
Effect.Create(fun (p: #IPaymentsProvider) -> p.Payments.Charge(customer, amount))
Stock.reserve is an Effect<#IStockProvider, unit, StockError>, and Payments.charge an
Effect<#IPaymentsProvider, string, PaymentError>.
Inside eff, binding an effect that fails ends the whole effect with that error, so the code only describes the
successful path. return! Error ... fails on purpose, and a plain Result can be bound like an effect:
let catalog = set [ "mug"; "plate" ]
let checkProduct sku =
if catalog.Contains sku then Ok sku else Error(UnknownProduct sku)
let reserveAll (lines: (string * int) list) = eff {
for sku, quantity in lines do
let! sku = checkProduct sku
do! Stock.reserve sku quantity
}
The first line that can't be reserved ends reserveAll, and the lines after it are never tried.
To run the examples, fake services are enough. The stock has two mugs and no plates, and the payment provider declines amounts over 100:
type FakeStock() =
let mutable mugs = 2
interface IStock with
member _.Reserve(sku, quantity) =
Task.FromResult(
match sku with
| "mug" when quantity <= mugs ->
mugs <- mugs - quantity
Ok()
| "mug"
| "plate" -> Error(OutOfStock sku)
| _ -> Error(UnknownProduct sku)
)
type FakePayments() =
interface IPayments with
member _.Charge(customer, amount) =
Task.FromResult(if amount > 100m then Error(Declined "limit") else Ok $"receipt for {customer}")
type Env() =
let stock = FakeStock()
let payments = FakePayments()
interface IStockProvider with
member _.Stock = stock
interface IPaymentsProvider with
member _.Payments = payments
/// Runs an effect with a new environment, and prints its result.
let show (effect: Effect<Env, 'a, 'e>) =
let result = effect |> Effect.run (Env())
printfn $"%A{result.AsTask().Result}"
reserveAll [ "mug", 1; "plate", 1; "mug", 1 ] |> show
reserveAll [ "mug", 1; "bowl", 1 ] |> show
|
Different errors
An effect has a single error type, so Stock.reserve and Payments.charge can't be bound in the same eff as they
are. The usual approach is a union for the workflow, with a case for each way it can fail, and Effect.changeError
to wrap the errors of the effects it uses:
type OrderError =
| Stock of StockError
| Payment of PaymentError
let placeOrder customer sku quantity price = eff {
do! Stock.reserve sku quantity |> Effect.changeError Stock
let! receipt = Payments.charge customer (decimal quantity * price) |> Effect.changeError Payment
return receipt
}
placeOrder "ada" "mug" 1 12m |> show
placeOrder "ada" "mug" 1 120m |> show
placeOrder "ada" "mug" 3 12m |> show
|
placeOrder is an Effect<#IStockProvider & #IPaymentsProvider, string, OrderError>: its callers see every way it
can fail, and nothing else. For a plain Result, Result.mapError does the same as Effect.changeError.
Handling errors
Matching on the result after running the effect handles errors at the composition root. Often an error is better
handled where it happens, and the Effect module has functions for that, each running only when the effect fails:
Effect.recoverturns the error into a value, so the effect always succeeds;Effect.tryRecoverturns it into aResult: a value, or an error;Effect.onErrorturns it into another effect, such as a fallback.
Here, a declined payment becomes a message for the customer, while other errors stay errors:
let placeOrderOrExplain customer sku quantity price =
placeOrder customer sku quantity price
|> Effect.tryRecover (function
| Payment(Declined reason) -> Ok $"payment declined ({reason}), the order was not placed"
| other -> Error other)
placeOrderOrExplain "ada" "mug" 1 120m |> show
|
Sometimes the decision depends on more than the error. Effect.asResult makes the outcome a value, to be matched on
inside eff, while the surrounding effect carries on:
let reserveOrBackorder sku quantity = eff {
match! Stock.reserve sku quantity |> Effect.asResult with
| Ok() -> return "reserved"
| Error(OutOfStock _) -> return "backordered"
| Error other -> return! Error other
}
reserveOrBackorder "mug" 5 |> show
|
Collecting errors
Binding one effect after another stops at the first error, which is usually what is wanted: there is no point
charging for an order that couldn't be reserved. When checking input, though, it is friendlier to report everything
that is wrong at once. and! binds effects together, running them in parallel where possible, and when more than
one of them fails, their errors are combined with the error type's + operator:
type Problems =
| Problems of string list
static member (+)(Problems a, Problems b) = Problems(a @ b)
let problem message = Error(Problems [ message ])
let checkCustomer (customer: string) = eff {
if String.IsNullOrWhiteSpace customer then
return! problem "the customer is missing"
else
return customer
}
let checkLine (sku: string) quantity = eff {
if quantity <= 0 then
return! problem $"the quantity of {sku} must be positive"
else
return sku, quantity
}
let checkOrder customer sku quantity = eff {
let! customer = checkCustomer customer
and! sku, quantity = checkLine sku quantity
return customer, sku, quantity
}
checkOrder "ada" "mug" 1 |> show
checkOrder "" "mug" 0 |> show
|
An error type without + can't be used with and!, which the compiler reports. Effect.zip combines two effects
the same way as and!. Effect.whenAll and Effect.par, which run any number of effects in parallel, don't
combine errors: they fail with the error of the first failing effect, in the order the effects were given.
Checking the input and placing the order fit together with one more case in OrderError:
type OrderError =
| Invalid of string list
| Stock of StockError
| Payment of PaymentError
let checkAndPlaceOrder customer sku quantity price = eff {
let! customer, sku, quantity =
checkOrder customer sku quantity |> Effect.changeError (fun (Problems problems) -> Invalid problems)
return! placeOrder customer sku quantity price
}
Exceptions
An exception thrown by an effect isn't turned into an error: it propagates, and the ValueTask returned by
Effect.run faults with it, as a Task would. try ... with works inside eff, for the exceptions that should
become errors, such as those of a client library that reports failures by throwing:
type IShipping =
abstract member Book: sku: string -> Task<string>
type IShippingProvider =
abstract member Shipping: IShipping
type ShippingError = ShippingUnavailable of message: string
let bookShipping sku = eff {
try
return! Effect.Create(fun (p: #IShippingProvider) -> p.Shipping.Book sku)
with :? System.Net.Http.HttpRequestException as e ->
return! Error(ShippingUnavailable e.Message)
}
type ShippingEnv() =
interface IShippingProvider with
member _.Shipping =
{ new IShipping with
member _.Book _ =
Task.FromException<string>(System.Net.Http.HttpRequestException "connection refused")
}
let booking = bookShipping "mug" |> Effect.run (ShippingEnv())
printfn $"%A{booking.AsTask().Result}"
|
Going the other way, Effect.runOrFail runs an effect and throws when it fails, with the error's string
representation as the message. It suits the edges of a program where a failure should end things, such as a script
or a test, while Effect.run, returning the error as a value, suits everywhere else.
val string: value: 'T -> string
--------------------
type string = String
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable + 1 overload member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 5 overloads ...
<summary>Represents an asynchronous operation.</summary>
--------------------
type Task<'TResult> = inherit Task new: ``function`` : Func<obj,'TResult> * state: obj -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable<'TResult> + 1 overload member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: cancellationToken: CancellationToken -> Task<'TResult> + 4 overloads member Result: 'TResult static member Factory: TaskFactory<'TResult>
<summary>Represents an asynchronous operation that can return a value.</summary>
<typeparam name="TResult">The type of the result produced by this <see cref="T:System.Threading.Tasks.Task`1" />.</typeparam>
--------------------
Task(action: Action) : Task
Task(action: Action, cancellationToken: Threading.CancellationToken) : Task
Task(action: Action, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj) : Task
Task(action: Action, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken) : Task
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task
--------------------
Task(``function`` : Func<'TResult>) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
module Result from Microsoft.FSharp.Core
--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val decimal: value: 'T -> decimal (requires member op_Explicit)
--------------------
type decimal = Decimal
--------------------
type decimal<'Measure> = decimal
union case Effect.Effect: EffectDelegate<'r,'a,'e> -> Effect<'r,'a,'e>
--------------------
module Effect from Orsak
<summary> Functions for running, combining, recovering and repeating effects. </summary>
--------------------
type Effect = static member Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e> + 2 overloads static member Error: error: 'e -> Effect<'a,'b,'e>
<summary> Creates effects. <c>Effect.Create</c> turns a function of an environment into an effect, and is how effects are made from the interfaces that describe side effects. The function may return a plain value, a <c>Result</c>, a <c>Task</c>, a <c>ValueTask</c> or an <c>Async</c>, of a value or of a <c>Result</c>; returning a <c>Result</c> lets the effect fail. </summary>
<example> The usual pattern: an interface describing the side effect, a provider interface exposing it, and a function creating the effect, which requires any environment that implements the provider (<c>#IConsoleProvider</c>). <code lang="fsharp"> type IConsole = abstract ReadLine: unit -> string abstract WriteLine: string -> unit type IConsoleProvider = abstract Console: IConsole module Console = let readLine () = Effect.Create(fun (p: #IConsoleProvider) -> p.Console.ReadLine()) let writeLine line = Effect.Create(fun (p: #IConsoleProvider) -> p.Console.WriteLine line) </code> The Orsak.Myriad generators can write the functions of such a module from the interface. </example>
--------------------
type Effect<'r,'a,'e> = | Effect of EffectDelegate<'r,'a,'e> member Run: r: 'r -> AsyncResult<'a,'e> member RunOrFail: r: 'r -> Task<'a> static member (<*>)<'b,'a (requires member (+))> : f: Effect<'r,('b -> 'a),'a0> * e: Effect<'r,'b,'a0> -> Effect<'r,'a,'a0> (requires member (+)) static member (>>=) : h: Effect<'r,'a,'e> * f: ('a -> Effect<'r,'b,'e>) -> Effect<'r,'b,'e> static member Map: h: Effect<'r,'a,'e> * f: ('a -> 'b) -> Effect<'r,'b,'e> static member Return<'a,'b,'c> : a: 'a0 -> Effect<'b,'a0,'c> static member ap<'r,'a,'b,'e> : applicative: Effect<'r,('b -> 'a),'e> -> e: Effect<'r,'b,'e> -> Effect<'r,'a,'e>
<summary> Describes an effect that can either succeed with <typeparamref name="'a" />, or fail with <typeparamref name="'e" />. The effect is 'cold', and only starts when run with an <typeparamref name="'r" />. </summary>
<remarks> Effects are usually written with the <c>eff</c> computation expression, and created from interfaces that describe side effects with <c>Effect.Create</c>. The environment <typeparamref name="'r" /> is inferred from what the effect uses, and supplied when the effect is run. </remarks>
<example><code lang="fsharp"> let greet () = eff { let! name = Console.readLine () do! Console.writeLine $"Hello {name}" } // at the composition root, with an environment providing the console effects: let! result = greet () |> Effect.run env </code></example>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
static member Effect.Create: f: ('a -> Async<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> Result<'b,'e>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Task<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> ValueTask<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> Async<Result<'b,'e>>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> ValueTask<Result<'b,'e>>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e>
<summary> The computation expression for writing effects. Binding (<c>let!</c>, <c>do!</c>, <c>return!</c>) works with other effects, and with <c>Task</c>, <c>ValueTask</c>, <c>Async</c> and <c>Result</c>, so .NET APIs can be used directly. It supports <c>for</c>, <c>while</c>, <c>use</c>, <c>try/with</c> and <c>try/finally</c>, and <c>and!</c> runs effects concurrently. A failed effect stops the rest of the expression, with its error. </summary>
<example><code lang="fsharp"> let placeOrder (order: Order) = eff { let! id = GuidGenerator.genGuid () let! now = Time.utcNow () do! Orders.save { order with Id = id; Placed = now } let! customer = Customers.load order.CustomerId and! stock = Stock.reserve order.Lines return id } </code></example>
type FakeStock = interface IStock new: unit -> FakeStock
--------------------
new: unit -> FakeStock
type FakePayments = interface IPayments new: unit -> FakePayments
--------------------
new: unit -> FakePayments
type Env = interface IPaymentsProvider interface IStockProvider new: unit -> Env
--------------------
new: unit -> Env
Runs an effect with a new environment, and prints its result.
<summary> Starts the effect. </summary>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
<param name="env">The environment needed to start the effect</param>
<param name="e">The effect to run</param>
<example><code lang="fsharp"> task { match! placeOrder order |> Effect.run env with | Ok id -> printfn $"Placed {id}" | Error err -> printfn $"Failed: {err}" } </code></example>
union case OrderError.Stock: StockError -> OrderError
--------------------
module Stock from Errors
<summary> Transforms an effect which fails with an error of <typeparamref name="'e1" /> to an effect which fails with an error of type <typeparamref name="'e2" />. </summary>
<param name="f">Function which maps the error, in case the effect had failed</param>
<param name="e">The effect</param>
<summary> Potentially recovers a failed effect by producing an <see cref="Result{'a, 'e}" /> in case of failure. If the effect was already successful, <paramref name="f" /> is never run. </summary>
<param name="f">The function to run in case of failure</param>
<param name="e">The effect</param>
<summary> Allows the effect to be bound as a result, rather than failing the surrounding effect when bound. This allows for taking recovery action inside the effect, based on the outcome of a sub-effect. </summary>
<param name="e">The effect</param>
union case Problems.Problems: string list -> Problems
--------------------
type Problems = | Problems of string list static member (+) : Problems * Problems -> Problems
type String = interface seq<char> interface IEnumerable interface ICloneable interface IComparable interface IComparable<string> interface IConvertible interface IEquatable<string> interface IParsable<string> interface ISpanParsable<string> new: value: nativeptr<char> -> unit + 8 overloads ...
<summary>Represents text as a sequence of UTF-16 code units.</summary>
--------------------
String(value: nativeptr<char>) : String
String(value: char array) : String
String(value: ReadOnlySpan<char>) : String
String(value: nativeptr<sbyte>) : String
String(c: char, count: int) : String
String(value: nativeptr<char>, startIndex: int, length: int) : String
String(value: char array, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : String
val string: value: 'T -> string
--------------------
type string = System.String
type HttpRequestException = inherit exn new: unit -> unit + 4 overloads member HttpRequestError: HttpRequestError member StatusCode: Nullable<HttpStatusCode>
<summary>A base class for exceptions thrown by the <see cref="T:System.Net.Http.HttpClient" /> and <see cref="T:System.Net.Http.HttpMessageHandler" /> classes.</summary>
--------------------
Net.Http.HttpRequestException() : Net.Http.HttpRequestException
Net.Http.HttpRequestException(message: string) : Net.Http.HttpRequestException
Net.Http.HttpRequestException(message: string, inner: exn) : Net.Http.HttpRequestException
Net.Http.HttpRequestException(message: string, inner: exn, statusCode: Nullable<Net.HttpStatusCode>) : Net.Http.HttpRequestException
Net.Http.HttpRequestException(httpRequestError: Net.Http.HttpRequestError, ?message: string, ?inner: exn, ?statusCode: Nullable<Net.HttpStatusCode>) : Net.Http.HttpRequestException
type ShippingEnv = interface IShippingProvider new: unit -> ShippingEnv
--------------------
new: unit -> ShippingEnv
Task.FromException(``exception`` : exn) : Task
Orsak