|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.
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.
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:
- Occurs when
sis a null reference.
- Occurs when
sdoes not represent a numeric input.
- Occurs when
srepresents number greater than or less than
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.
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 -> 'a
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' already been added to tree" x
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()
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 ExceptionsEdit
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 ConstructsEdit
||F# library function||Raises the given exception|
||F# library function||Raises the
||F# expression||Catches expressions matching the pattern rules|
||F# expression||Execution the
||F# pattern rule||A rule matching the given .NET exception type|
||F# pattern rule||A rule matching the given .NET exception type, binding the name
||F# pattern rule||A rule matching the given data-carrying F# exception|
||F# pattern rule||A rule matching any exception, binding the name
||F# pattern rule||A rule matching the exception under the given condition, binding the name