Reputation: 323
I’m moving away from creating and catching exceptions in F# to something built around Result<'T, 'TError>
. I found this, which agrees with my initial pursuit of representing failures with a discriminated union, but I ran into the problem of having a lot of different cases for my Failure
discriminated union:
type TypedValue =
| Integer of int
| Long of int64
| …
type Failure =
| ArgumentOutOfRange of {| Argument : TypedValue; Minimum : TypedValue; Maximum : TypedValue |}
| BufferTooSmall of {| RequiredSize : int |}
| Exception of exn
| IndexOutOfRange of {| Index : int |}
| …
I’d prefer not to have a multitude of types dedicated to error handling. This “typed value” thing is not elegant at all as I either have to create conflicting names (Byte
versus System.Byte
) or create long names to avoid conflict (| UnsignedByte of byte
).
Generics is a possibility, but then what would the 'T
in Failure<'T>
represent? ArgumentOutOfRange
wouldn’t be the only case in the discriminated union, and some cases might require more type parameters or none at all.
Upvotes: 2
Views: 340
Reputation: 6510
Another option (and what I normally do, personally) is to model your domain-specific failures with specific cases in your Failure
union, and then have a general-purpose UnexpectedError
case that takes an exn
as its data and handles any non-domain-related failures. Then, when an error from one domain occurs in another, you can use Result.mapError
to convert between them. Here's an example from a real domain I've modeled:
open System
// Top-level domain failures
type EntityValidationError =
| EntityIdMustBeGreaterThanZero of int64
| InvalidTenant of string
| UnexpectedException of exn
// Sub-domain specific failures
type AccountValidationError =
| AccountNumberMustBeTenDigits of string
| AccountNameIsRequired of string
| EntityValidationError of EntityValidationError // Sub-domain representaiton of top-level failures
| AccountValidationUnexpectedException of exn
// Sub-domain Entity
// The fields would probably be single-case unions rather than primitives
type Account =
{
Id: int64
AccountNumber: string
}
module EntityId =
let validate id =
if id > 0L
then Ok id
else Error (EntityIdMustBeGreaterThanZero id)
module AccountNumber =
let validate number =
if number |> String.length = 10 && number |> Seq.forall Char.IsDigit
then Ok number
else Error (AccountNumberMustBeTenDigits number)
module Account =
let create id number =
id
|> EntityId.validate
|> Result.mapError EntityValidationError // Convert to sub-domain error type
|> Result.bind (fun entityId ->
number
|> AccountNumber.validate
|> Result.map (fun accountNumber -> { Id = entityId; AccountNumber = accountNumber }))
Upvotes: 1
Reputation: 243096
Using Result<'T, 'TError>
makes a lot of sense in cases where you have custom kinds of errors that you definitely need to handle or in cases where you have some other logic for propagating errors than the one implemented by standard exceptions (e.g. if you can continue running code despite the fact that there was an error). However, I would not use it as a 1:1 replacement for exceptions - it will just make your code unnecessarilly complicated and cumbersome without really giving you much benefits.
To answer your question, since you are mirroring standard .NET exceptions in your discriminated union, you could probably just use a standard .NET exception in your Result
type and use Result<'T, exn>
as your data type:
if arg < 10 then Error(ArgumentOutOfRangeException("arg", "Value is too small"))
else OK(arg - 1)
Regarding the ArgumentOutOfRange
union case and TypedValue
- the reason for using something like TypedValue
is typically that you need to pattern match on the possible values and do something with them. In case of exceptions, what do you want to do with the values? If you just need to report them to the user, then you can use obj
which will let you easily print them (it won't be that easy to get the numerical values and do some further calculations with them, but I don't think you need that).
type Failure =
| ArgumentOutOfRange of {| Argument : obj; Minimum : obj; Maximum : obj |}
Upvotes: 3