Exception Handling

F Sharp Programming · Wikibooks

0%

Contents

    Text

    Size
    100%
    Theme
    Typeface
    Line length
    Spacing

    From Wikibooks, under CC BY-SA 4.0, as the source states it. This page at Wikibooks.

    Exception Handling
    Previous: Input and Output Index Next: Operator Overloading
    F# : Exception Handling

    When a program encounters a problem or enters an invalid state, it will often respond by throwing an exception. Left to its own devices, an uncaught exception will crash an application. Programmers write exception handling code to rescue an application from an invalid state.

    Try/With

    Let's look at the following code:

    let getNumber msg = printf msg; int32(System.Console.ReadLine())
    
    let x = getNumber("x = ")
    let y = getNumber("y = ")
    printfn "%i + %i = %i" x y (x + y)
    

    This code is syntactically valid, and it has the correct types. However, it can fail at run time if we give it a bad input:

    This program outputs the following:

    x = 7
    y = monkeys!
    ------------
    FormatException was unhandled. Input string was not in a correct format.
    

    The string monkeys does not represent a number, so the conversion fails with an exception. We can handle this exception using F#'s try... with, a special kind of pattern matching construct:

    let getNumber msg =
        printf msg;
        try
            int32(System.Console.ReadLine())
        with
            | :? System.FormatException -> System.Int32.MinValue
    
    let x = getNumber("x = ")
    let y = getNumber("y = ")
    printfn "%i + %i = %i" x y (x + y)
    

    This program outputs the following:

    x = 7
    y = monkeys!
    7 + -2147483648 = -2147483641
    

    It is, of course, wholly possible to catch multiple types of exceptions in a single with block. For example, according to the MSDN documentation, the System.Int32.Parse(s : string) method will throw three types of exceptions:

    ArgumentNullException

    Occurs when s is a null reference.

    FormatException

    Occurs when s does not represent a numeric input.

    OverflowException

    Occurs when s represents number greater than or less than Int32.MaxValue or Int32.MinValue (i.e. the number cannot be represented with a 32-bit signed integer).

    We can catch all of these exceptions by adding additional match cases:

    let getNumber msg =
        printf msg;
        try
            int32(System.Console.ReadLine())
        with
            | :? System.FormatException -> -1
            | :? System.OverflowException -> System.Int32.MinValue
            | :? System.ArgumentNullException -> 0
    

    Its not necessary to have an exhaustive list of match cases on exception types, as the uncaught exception will simply move to the next method in the stack trace.

    Raising Exceptions

    The code above demonstrates how to recover from an invalid state. However, when designing F# libraries, its often useful to throw exceptions to notify users that the program encountered some kind of invalid input. There are several standard functions for raising exceptions:

    (* General failure *)
    val failwith : string -> 'a
    
    (* General failure with formatted message *)
    val failwithf : StringFormat<'a, 'b> -> 'a
    
    (* Raise a specific exception *)
    val raise : #exn -> 'a
    
    (* Bad input *)
    val invalidArg : string -> string -> 'a
    

    For example:

    type 'a tree =
        | Node of 'a * 'a tree * 'a tree
        | Empty
        
    let rec add x = function
        | Empty -> Node(x, Empty, Empty)
        | Node(y, left, right) ->
            if x > y then Node(y, left, add x right)
            else if x < y then Node(y, add x left, right)
            else failwithf "Item '%A' has already been added to tree" x
    

    Try/Finally

    Normally, an exception will cause a function to exit immediately. However, a finally block will always execute, even if the code throws an exception:

    let tryWithFinallyExample f =
        try
            printfn "tryWithFinallyExample: outer try block"
            try
                printfn "tryWithFinallyExample: inner try block"
                f()
            with
                | exn ->
                    printfn "tryWithFinallyExample: inner with block"
                    reraise() (* raises the same exception we just caught *)
        finally
            printfn "tryWithFinally: outer finally block"
            
    let catchAllExceptions f =
        try
            printfn "-------------"
            printfn "catchAllExceptions: try block"
            tryWithFinallyExample f
        with
            | exn ->
                printfn "catchAllExceptions: with block"
                printfn "Exception message: %s" exn.Message
        
    let main() =                
        catchAllExceptions (fun () -> printfn "Function executed successfully")
        catchAllExceptions (fun () -> failwith "Function executed with an error")
        
    main()
    

    This program will output the following:

    -------------
    catchAllExceptions: try block
    tryWithFinallyExample: outer try block
    tryWithFinallyExample: inner try block
    Function executed successfully
    tryWithFinally: outer finally block
    -------------
    catchAllExceptions: try block
    tryWithFinallyExample: outer try block
    tryWithFinallyExample: inner try block
    tryWithFinallyExample: inner with block
    tryWithFinally: outer finally block
    catchAllExceptions: with block
    Exception message: Function executed with an error

    Notice that our finally block executed in spite of the exception. Finally blocks are used most commonly to clean up resources, such as closing an open file handle or closing a database connection (even in the event of an exception, we do not want to leave file handles or database connections open):

    open System.Data.SqlClient
    let executeScalar connectionString sql =
        let conn = new SqlConnection(connectionString)
        try
            conn.Open() (* this line can throw an exception *)
            let comm = new SqlCommand(sql, conn)
            comm.ExecuteScalar() (* this line can throw an exception *)
        finally
            (* finally block guarantees our SqlConnection is closed, even if our sql statement fails *)
            conn.Close()
    

    use Statement

    Many objects in the .NET framework implement the System.IDisposable interface, which means the objects have a special method called Dispose to guarantee deterministic cleanup of unmanaged resources. It's considered a best practice to call Dispose on these types of objects as soon as they are no longer needed.

    Traditionally, we'd use a try/finally block in this fashion:

    let writeToFile fileName =
        let sw = new System.IO.StreamWriter(fileName : string)
        try
            sw.Write("Hello ")
            sw.Write("World!")
        finally
            sw.Dispose()
    

    However, this can be occasionally bulky and cumbersome, especially when dealing with many objects which implement the IDisposable interface. F# provides the keyword use as syntactic sugar for the pattern above. An equivalent version of the code above can be written as follows:

    let writeToFile fileName =
        use sw = new System.IO.StreamWriter(fileName : string)
        sw.Write("Hello ")
        sw.Write("World!")
    

    The scope of a use statement is identical to the scope of a let statement. F# will automatically call Dispose() on an object when the identifier goes out of scope.

    Defining New Exceptions

    F# allows us to easily define new types of exceptions using the exception declaration. Here's an example using fsi:

    > exception ReindeerNotFoundException of string
    
    let reindeer =
        ["Dasher"; "Dancer"; "Prancer"; "Vixen"; "Comet"; "Cupid"; "Donner"; "Blitzen"]
        
    let getReindeerPosition name =
        match List.tryFindIndex (fun x -> x = name) reindeer with
        | Some(index) -> index
        | None -> raise (ReindeerNotFoundException(name));;
    
    exception ReindeerNotFoundException of string
    val reindeer : string list
    val getReindeerPosition : string -> int
    
    > getReindeerPosition "Comet";;
    val it : int = 4
    
    > getReindeerPosition "Donner";;
    val it : int = 6
    
    > getReindeerPosition "Rudolf";;
    FSI_0033+ReindeerNotFoundExceptionException: Rudolf
       at FSI_0033.getReindeerPosition(String name)
       at <StartupCode$FSI_0036>.$FSI_0036._main()
    stopped due to error
    

    We can pattern match on our new existing exception type just as easily as any other exception:

    > let tryGetReindeerPosition name =
        try
            getReindeerPosition name
        with
            | ReindeerNotFoundException(s) ->
                printfn "Got ReindeerNotFoundException: %s" s
                -1;;
    
    val tryGetReindeerPosition : string -> int
    
    > tryGetReindeerPosition "Comet";;
    val it : int = 4
    
    > tryGetReindeerPosition "Rudolf";;
    Got ReindeerNotFoundException: Rudolf
    val it : int = -1
    

    Exception Handling Constructs

    Construct Kind Description
    raise expr F# library function Raises the given exception
    failwith expr F# library function Raises the System.Exception exception
    try expr with rules F# expression Catches expressions matching the pattern rules
    try expr finally expr F# expression Execution the finally expression both when the computation is successful and when an exception is raised
    | :? ArgumentException F# pattern rule A rule matching the given .NET exception type
    | :? ArgumentException as e F# pattern rule A rule matching the given .NET exception type, binding the name e to the exception object value
    | Failure(msg) -> expr F# pattern rule A rule matching the given data-carrying F# exception
    | exn -> expr F# pattern rule A rule matching any exception, binding the name exn to the exception object value
    | exn when expr -> expr F# pattern rule A rule matching the exception under the given condition, binding the name exn to the exception object value
    Previous: Input and Output Index Next: Operator Overloading