F# Training
F# Training
F# Training
  • Presentation
  • Fundamentals
    • Introduction
    • Syntax
      • Bases
      • Functions
      • Rules
      • Exceptions
    • First concepts
    • šŸ”Quiz
  • Functions
    • Signature
    • Concepts
    • Syntax
    • Standard functions
    • Operators
    • Addendum
    • šŸ”Quiz
    • šŸ“œSummary
  • Types
    • Overview
    • Tuples
    • Records
    • Unions
    • Enums
    • Anonymous records
    • Value types
    • šŸ“œRecap
    • Addendum
  • Monadic types
    • Intro
    • Option type
    • Result type
    • Smart constructor
    • šŸš€Computation expression (CE)
    • šŸš€CE theoretical basements
    • šŸ“œRecap
  • Pattern matching
    • Patterns
    • Match expression
    • Active patterns
    • šŸš€Fold function
    • šŸ“œRecap
    • šŸ•¹ļøExercises
  • Collections
    • Overview
    • Types
    • Common functions
    • Dedicated functions
    • šŸ”Quiz
    • šŸ“œRecap
  • Asynchronous programming
    • Asynchronous workflow
    • Interop with .NET TPL
    • šŸ“œRecap
  • Module and Namespace
    • Overview
    • Namespace
    • Module
    • šŸ”Quiz
    • šŸ“œRecap
  • Object-oriented
    • Introduction
    • Members
    • Type extensions
    • Class, Struct
    • Interface
    • Object expression
    • Recommendations
Powered by GitBook
On this page
  • Type alias
  • Defining exceptions
  • Throwing exceptions
  • Handling exceptions
  • Full example

Was this helpful?

Edit on GitHub
  1. Fundamentals
  2. Syntax

Exceptions

PreviousRulesNextFirst concepts

Last updated 21 days ago

Was this helpful?

In F♯, thanks to Unions šŸ“, we can deal with expected errors using the Result type and optional union types for custom error - see šŸ”— .

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 chain

    • equivalent of throw; in C♯

    • preserve the stack trace, contrary to raise ex

  • failwith (message: string) and failwithf (messageTemplate: string) (...args)

    • generate a general F♯ exception with the given message

    • failwith is the shortcut for raise (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 message

    • shortcut for raise (ArgumentException(message, argumentName))

  • nullArg (argumentName: string)

    • generate a System.NullArgumentException for the given argument name

    • shortcut for raise (ArgumentNullException argumentName)

  • invalidOp (message: string)

    • generate a System.InvalidOperationException with the given message

    • shortcut 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, not catch, contrary to C♯.

  • There is no try/with/finally expression, only try/finally that we can nest in another try/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 by failwith(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 "---"
)
Outputs
---
šŸ’£ Failure(Message = Unknown error)
Input was: FailWith "Unknown error"
---
šŸ’£ Exception(Type = NotImplementedException, Message = The method or operation is not implemented.)
Input was: NotImplemented
---
šŸ’£ FormatException(Message = The input string 'not a number' was not in a correct format.)
Input was: NotANumber "not a number"
---
šŸ’£ ArgumentMustBePositiveOrZeroException(Name = userId, Value = -1)
Input was: NotPositive ("userId", -1)
---
šŸ’£ ArgumentNullException(ParamName = userId, Message = Value cannot be null. (Parameter 'userId'))
Input was: Null "userId"
---
āœ… userId 3 is valid
Input was: Valid ("userId", 3)
---

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 and ArgumentMustBePositiveOrZeroException patterns can be placed anywhere above the last pattern exn because they target specific types.

  • The pattern exn must be placed at the end because it's the most general one.

Deconstruction patterns

Failure and ArgumentMustBePositiveOrZeroException patterns support deconstruction: → We can access the exception property elegantly compared to ?: XxxException as exn -> exn.Xxx.

to do the same outside of a (try...) with block, it's more complicated → šŸ”—

Railway oriented programming
https://stackoverflow.com/a/41202215/8634147