Basics 12 - Exceptions and Pattern Matching

There are two kinds of exceptions when working in F#:

  1. .NET exceptions, which are driven from System.Exception. Some examples are:

    1. System.DivideByZeroException

    2. System.ArgumentException

    3. System.NullReferenceException

  2. F# exceptions

F# Exceptions

F# exceptions are defined using the exception keyword.

// An exception type for holding a message
exception ValidationError of string

// An exception type for holding a list of messages
exception ValidationError of string list

// An exception type for holding message and error code
exception BusinessError of message: string * code: int

Raising/Throwing Exceptions

Exceptions (of both types) in F# are raised using raise function.

// Raise System.Exception
raise (System.Exception())

// Raise System.ArgumentException
raise (System.ArgumentException("I don't like your argument!"))

// Raise ValidationError
raise (ValidationError("I don't like whatever you are supplying."))

// Raise BusinessError
raise (BusinessError("I don't like whatever condition we are in.", 10))

Handling/Catching Exceptions

Exception handling in F# is done with try/with expression, which is very similar to pattern matching.

let answer =
    try
        ...
    with
    | ValidationError(message) -> ...
    | BusinessError(message, code) -> ...
    | :? System.NullReferenceException as ex -> ...
    | :? System.DivideByZeroException as ex -> ...
    | ex -> ...
  1. try/with is an expression, which may:

    1. Like any expression, result in one value

    2. Raise/throw an exception from try block

    3. Reraise/rethrow from with block (discussed below)

  2. Typically, do whatever is required in try block

  3. F# exceptions (defined with exception keyword) are handled like discriminated union cases

  4. .NET exceptions are handled with :?

  5. | :? System.NullReferenceException as ex -> can be written as | :? System.NullReferenceException -> if you don't need the exception details

  6. The last line in the above-given code is wildcard/default case. ex is of type exn which is an alias for System.Exception.

  7. | ex -> can be written as | _ -> if you don't need the exception details

Here's another example:

let x = 4
let y = 2

let answer =
    try
        $"The result of {x}/{y} is {x / y}."
    with
    | :? System.DivideByZeroException -> $"Sorry! Can't divide by zero."
    | ex -> $"Sorry! The operation failed for an unknown reason. {ex}"

printf "%s" answer

The above-given example with pipeline:

let x = 4
let y = 0

try
    $"The result of {x}/{y} is {x / y}."
with
| :? System.DivideByZeroException -> $"Sorry! Can't divide by zero."
| ex -> $"Sorry! The operation failed for an unknown reason. {ex}"
|> printf "%s"

Reraising Exceptions

Exceptions are reraised from with block.

let x = 4
let y = 2

let answer =
    try
        $"The result of {x}/{y} is {x / y}."
    with
    | :? System.DivideByZeroException -> $"Sorry! Can't divide by zero."
    | _ -> reraise ()

printf "%s" answer

Typically, you reraise an exception from a function when you want the caller to handle the exception. In this example, if we hit | _ -> wildcard case, we have no idea what to do in this unexpected case. Another case of rethrowing is when you want to handle the exception (say logging), but let the caller also know.

If you have reached so far, congratulations.

Keep reading!