Skip to content
AgileLogics
6 min read

Exceptions

Prerequisites: Result and error handling

F# uses .NET exceptions for unexpected failures, and you'll meet them whenever you call .NET libraries.

try ... with

let safeDivide a b =
    try
        Some (a / b)
    with
    | :? System.DivideByZeroException -> None

printfn "%A" (safeDivide 10 2)
printfn "%A" (safeDivide 10 0)

let parse (s: string) =
    try
        int s
    with ex ->
        printfn "Error: %s" ex.Message
        0

printfn "%d" (parse "12")
printfn "%d" (parse "twelve")
Some 5
None
12
Error: The input string 'twelve' was not in a correct format.
0

try ... with is an expression: both the try body and each handler must return the same type. :? SomeException matches by exception type; ex alone catches everything.

Raising exceptions

let checkAge age =
    if age < 0 then invalidArg (nameof age) "must not be negative"
    elif age > 150 then failwith $"unrealistic age: {age}"
    else age

for age in [30; -1; 200] do
    try
        printfn "ok: %d" (checkAge age)
    with
    | :? System.ArgumentException as ex -> printfn "argument problem: %s" ex.Message
    | Failure message -> printfn "failure: %s" message
ok: 30
argument problem: must not be negative (Parameter 'age')
failure: unrealistic age: 200
  • failwith "message" raises a general exception; the Failure pattern catches it and extracts the message.
  • invalidArg, invalidOp, and nullArg raise the standard .NET argument exceptions. nameof age gives the string "age", which survives renaming.
  • raise (SomeException(...)) raises any exception object.

Your own exceptions

exception InsufficientFunds of balance: decimal * requested: decimal

let withdraw balance amount =
    if amount > balance then raise (InsufficientFunds (balance, amount))
    balance - amount

try
    printfn "%M" (withdraw 100m 30m)
    printfn "%M" (withdraw 100m 300m)
with
| InsufficientFunds (b, r) -> printfn "Can't withdraw %M from %M" r b
70
Can't withdraw 300 from 100

finally and use

try ... finally runs cleanup code whether or not an exception happened. For objects that hold resources (files, connections), use does this automatically — it calls Dispose when the value goes out of scope:

type Resource(name: string) =
    do printfn "open %s" name
    interface System.IDisposable with
        member _.Dispose() = printfn "close %s" name

let work () =
    use r = new Resource("database")
    printfn "working"
    failwith "something broke"

try
    try
        work ()
    finally
        printfn "finally block ran"
with Failure msg ->
    printfn "caught: %s" msg
open database
working
close database
finally block ran
caught: something broke

Notice the order: the resource closes as soon as the exception leaves work, then the finally block runs, then the handler.

reraise

Inside a handler, reraise () rethrows the current exception with its original stack trace — use it after logging:

try
    try
        failwith "disk full"
    with ex ->
        printfn "log: %s" ex.Message
        reraise ()
with ex ->
    printfn "outer handler: %s" ex.Message
log: disk full
outer handler: disk full

Common mistakes

  • Catching every exception and returning a default value hides bugs. Catch specific exceptions, or convert them to a Result at the edge of your program.
  • Using exceptions for normal control flow ("user not found") — that's what Option and Result are for.