Exceptions
In F♯, thanks to Unions 📍, we can deal with expected errors using the Result type and optional union types for custom error - see 🔗 Railway oriented programming.
Still, as exceptions are quite common in .NET, F♯ supports them fully and elegantly.
Type alias
F♯ provides the convenientexn
alias for the System.Exception
type.
Defining exceptions
To define our own exceptions, we can use the exception
keyword:
// Without inner data
exception ArgumentNullOrWhiteSpaceException
// With inner data
exception MustBePositiveException of int
// With inner data with labels
exception ArgumentMustBePositiveException of name: string * value: int
☝️ Notes:
Naming convention: the exception name should end with
Exception
Data labels are recommended for code clarity.
The syntax is close to a union type case for the exception's definition and its handling with pattern matching - see Full example below.
We can also define exceptions by inheriting from
System.Exception
. This is the C♯ style, but it is not recommended because it's less elegant for both definition and handling.
Throwing exceptions
F♯ provides a set of convenient helpers:
raise (exn: exn)
throw a custom exception object
equivalent of
throw exn;
in C♯
reraise ()
used in a
(try...) with
block to propagate a handled exception up the call chainequivalent of
throw;
in C♯preserve the stack trace, contrary to
raise ex
to do the same outside of a
(try...) with
block, it's more complicated → 🔗 https://stackoverflow.com/a/41202215/8634147
failwith (message: string)
andfailwithf (messageTemplate: string) (...args)
generate a general F♯ exception with the given message
failwith
is the shortcut forraise (Exception message)
failwithf
is less used since F♯ supports string interpolation.
invalidArg (argumentName: string) (message: string)
generate a
System.ArgumentException
for the given argument name and with the specified messageshortcut for
raise (ArgumentException(message, argumentName))
nullArg (argumentName: string)
generate a
System.NullArgumentException
for the given argument nameshortcut for
raise (ArgumentNullException argumentName)
invalidOp (message: string)
generate a
System.InvalidOperationException
with the given messageshortcut for
raise (InvalidOperationException message)
Examples:
let notImplemented () =
failwith "Not implemented"
let notNull argumentName value =
if isNull value then
nullArg argumentName
let divide x y =
match y with
| 0.0 -> invalidArg (nameof y) "Divisor cannot be zero"
| _ -> x / y
Handling exceptions
→ try/with
expression
let tryDivide x y =
try
Some (x / y)
with :? System.DivideByZeroException ->
None
☝️ Notes:
The keyword used is
with
, notcatch
, contrary to C♯.There is no
try/with/finally
expression, onlytry/finally
that we can nest in anothertry/with
.The
with
block is using pattern matching::? ExceptionType
to check if the handled exception has or derives from the given type.Failure message
is an active pattern to catch low-level exceptions that have exactly theSystem.Exception
type. → useful to handle exceptions raised byfailwith(f)
helpers.Exceptions defined using the
exception
keyword can be deconstructed → see Full example below.
Full example
The following example demonstrates almost every topic we studied:
How to define an exception type
How to raise an exception using different helpers
How to catch the exception and pattern match it
open System
exception ArgumentMustBePositiveOrZeroException of name: string * value: int
type Test =
| FailWith of message: string
| NotANumber of value: string
| NotImplemented
| NotPositive of name: string * value: int
| Null of name: string
| Valid of name: string * value: int
printfn "---"
[ FailWith "Unknown error"
NotImplemented
NotANumber "not a number"
NotPositive("userId", -1)
Null "userId"
Valid("userId", 3) ]
|> List.iter (fun input ->
try
try
match input with
| FailWith message -> failwith message
| NotANumber value -> Int32.Parse value |> ignore
| NotImplemented -> raise (NotImplementedException())
| NotPositive (name, value) -> raise (ArgumentMustBePositiveOrZeroException(name, value))
| Null name -> nullArg name
| Valid (name, value) -> printfn $"✅ %s{name} %d{value} is valid"
with
| :? ArgumentNullException as exn ->
printfn $"💣 ArgumentNullException(ParamName = %s{exn.ParamName}, Message = %s{exn.Message})"
| :? ArgumentException as exn ->
printfn $"💣 ArgumentException(ParamName = %s{exn.ParamName}, Message = %s{exn.Message})"
| :? FormatException as exn -> printfn $"💣 FormatException(Message = {exn.Message})"
| ArgumentMustBePositiveOrZeroException (name, value) ->
printfn $"💣 ArgumentMustBePositiveOrZeroException(Name = %s{name}, Value = %d{value})"
| Failure message ->
printfn $"💣 Failure(Message = %s{message})"
| exn ->
printfn $"💣 Exception(Type = %s{exn.GetType().Name}, Message = %s{exn.Message})"
finally
printfn "Input was: %A" input
printfn "---"
)
Pattern order
Pay attention to pattern order
:? ArgumentNullException
is placed before:? ArgumentException
because of the type inheritance relationship. Still, we are covered by the compiler: in case the 2 patterns are placed in the wrong order, we get the following warning:This rule will never be matched
Failure
andArgumentMustBePositiveOrZeroException
patterns can be placed anywhere above the last patternexn
because they target specific types.The pattern
exn
must be placed at the end because it's the most general one.
Last updated
Was this helpful?