You are previewing Programming F#.

Programming F#

Cover of Programming F# by Chris Smith Published by O'Reilly Media, Inc.
O'Reilly logo

Exceptions

Every programmer knows things don’t always happen as planned. I’ve been careful in examples so far to avoid showing you code where things fail. But dealing with the unexpected is a crucial aspect of any program, and fortunately the .NET platform supports a powerful mechanism for handling the unexpected.

An exception is a failure in a .NET program that causes an abnormal branch in control flow. If an exception occurs, the function immediately exits, as well as its calling function, and so on until the exception is caught by an exception handler. If the exception is never caught, the program terminates.

The simplest way to report an error in an F# program is to use the failwith function. This function takes a string as a parameter, and when called, throws an instance of System.Exception. An alternate version calls failwithf that takes a format string similar to printf and sprintf:

> // Using failwithf
let divide x y =
    if y = 0 then failwithf "Cannot divide %d by zero!" x
    x / y;;

val divide : int -> int -> int

> divide 10 0;;
System.Exception: Cannot divide 10 by zero!
   at FSI_0003.divide(Int32 x, Int32 y)
   at <StartupCode$FSI_0004>.$FSI_0004._main()
stopped due to error

In the previous example FSI indicated an exception was thrown, displaying the two most important properties on an exception type: the exception message and stack trace. Each exception has a Message property, which is a programmer-friendly description of the problem. The Stacktrace property is a string printout of all the functions waiting on a return value before the exception occurred, and is invaluable for tracking down the origin of an exception. Because the stack unwinds immediately after an exception is thrown, the exception could be caught far away from where the exception originated.

While a descriptive message helps programmers debug the exception, it is a best practice in .NET to use a specific exception type. To throw a more specific exception, you use the raise function. This takes a custom exception type (any type derived from System.Exception) and throws the exception just like failwith:

> // Raising a DivideByZeroException
let divide2 x y =
    if y = 0 then raise <| new System.DivideByZeroException()
    x / y;;

val divide2 : int -> int -> int

> divide2 10 0;;
System.DivideByZeroException: Attempted to divide by zero.
   at FSI_0005.divide2(Int32 x, Int32 y)
   at <StartupCode$FSI_0007>.$FSI_0007._main()
stopped due to error

Warning

It is tempting to throw exceptions whenever your program reaches an unexpected state; however, throwing exceptions incurs a significant performance hit. Whenever possible, situations that would throw an exception should be obviated.

Handling Exceptions

To handle an exception you catch it using a try-catch expression. Any exceptions raised while executing code within a try-catch expression will be handled by a with block, which is a pattern match against the exception type.

Because the exception handler to execute is determined by pattern matching, you can combine exception handlers using Or or use a wildcard to catch any exception. If an exception is thrown within a try-catch expression and an appropriate exception handler cannot be found, the exception will continue bubbling up until caught or the program terminates.

try-catch expressions return a value, just like a pattern match or if expression. So naturally the last expression in the try block must have the same type as each rule in the with pattern match.

Example 4-14 shows some code that runs through a minefield of potential problems. Each possible exception will be caught with an appropriate exception handler. In the example, the :? dynamic type test operator is used to match against the exception type; this operator will be covered in more detail in the next chapter.

Example 4-14. try-catch expressions

open System.IO

[<EntryPoint>]
let main (args : string[]) =

    let exitCode =
        try
            let filePath = args.[0]

            printfn "Trying to gather information about file:"
            printfn "%s" filePath

            // Does the drive exist?
            let matchingDrive =
                Directory.GetLogicalDrives()
                |> Array.tryFind (fun drivePath -> drivePath.[0] = filePath.[0])

            if matchingDrive = None then
                raise <| new DriveNotFoundException(filePath)

            // Does the folder exist?
            let directory = Path.GetPathRoot(filePath)
            if not <| Directory.Exists(directory) then
                raise <| new DirectoryNotFoundException(filePath)

            // Does the file exist?
            if not <| File.Exists(filePath) then
                raise <| new FileNotFoundException(filePath)

            let fileInfo = new FileInfo(filePath)
            printfn "Created  = %s" <| fileInfo.CreationTime.ToString()
            printfn "Access   = %s" <| fileInfo.LastAccessTime.ToString()
            printfn "Size     = %d" fileInfo.Length

            0

        with
        // Combine patterns using Or
        | :? DriveNotFoundException
        | :? DirectoryNotFoundException
            ->  printfn "Unhandled Drive or Directory not found exception"
                1
        | :? FileNotFoundException as ex
            ->  printfn "Unhandled FileNotFoundException: %s" ex.Message
                3
        | :? IOException as ex
            ->  printfn "Unhandled IOException: %s" ex.Message
                4
        // Use a wild card match (result will be of type System.Exception)
        | _ as ex
            ->  printfn "Unhandled Exception: %s" ex.Message
                5

    // Return the exit code
    printfn "Exiting with code %d" exitCode
    exitCode

Because not catching an exception might prevent unmanaged resources from being freed, such as file handles not being closed or flushing buffers, there is a second way to catch process exceptions: try-finally expressions. In a try-finally expression, the code in the finally block is executed whether or not an exception is thrown, giving you an opportunity to do required cleanup work.

Example 4-15 demonstrates a try-finally expression in action.

Example 4-15. try-finally expressions

> // Try-finally expressions
let tryFinallyTest() =
    try
        printfn "Before exception..."
        failwith "ERROR!"
        printfn "After exception raised..."
    finally
        printfn "Finally block executing..."

let test() =
    try
        tryFinallyTest()
    with
    | ex -> printfn "Exception caught with message: %s" ex.Message;;

val tryFinallyTest : unit -> unit
val test : unit -> unit

> test();;
Before exception...
Finally block executing...
Exception caught with message: ERROR!
val it : unit = ()

Note

Unlike in C#, there is no try-catch-finally expression. If you need to clean up any resources within an exception handler, you must do it for each exception handler or simply after the try-catch block.

Reraising Exceptions

Sometimes, despite your best efforts to take corrective action, you just can’t fix the problem. In those situations, you can reraise the exception, which will allow the original exception to continue bubbling up from within an exception handler.

Example 4-16 demonstrates reraising an exception by using the reraise function.

Example 4-16. Reraise exceptions

open Checked

let reraiseExceptionTest() =
    try
        let x = 0x0fffffff
        let y = 0x0fffffff

        x * y
    with
    | :? System.OverflowException as ex
        ->  printfn "An overflow exception occured..."
            reraise()

Defining Exceptions

Throwing specialized exceptions is key for consumers of your code to only catch the exceptions they know how to handle. Other exceptions will then continue to bubble up until an appropriate exception handler is found.

You can define your own custom exceptions by creating types that inherit from System.Exception, which you will see in Chapter 5. However, in F#, there is an easier way to define exceptions using a lightweight exception syntax. Declaring exceptions in this way allows you to define them with the same syntax as discriminated unions.

Example 4-17 shows creating several new exception types, some of which are associated with data. The advantage of these lightweight exceptions is that when they are caught, it is easier to extract the relevant data from them because you can use the same syntax for pattern matching against discriminated unions. Also, there is no need for a dynamic type test operator :?, which we saw in previous examples.

Example 4-17. Lightweight F# exception syntax

open System
open System.Collections.Generic

exception NoMagicWand
exception NoFullMoon of int * int
exception BadMojo of string

let castHex (ingredients : HashSet<string>) =
    try

        let currentWand = Environment.MagicWand

        if currentWand = null then
            raise NoMagicWand

        if not <| ingredients.Contains("Toad Wart") then
            raise <| BadMojo("Need Toad Wart to cast the hex!")

        if not <| isFullMoon(DateTime.Today) then
            raise <| NoFullMoon(DateTime.Today.Month, DateTime.Today.Day)

        // Begin the incantation...
        let mana =
            ingredients
            |> Seq.map (fun i -> i.GetHashCode())
            |> Seq.fold (+) 0

        sprintf "%x" mana

    with
    | NoMagicWand
        -> "Error: A magic wand is required to hex!"
    | NoFullMoon(month, day)
        -> "Error: Hexes can only be cast during a full moon."
    | BadMojo(msg)
        -> sprintf "Error: Hex failed due to bad mojo [%s]" msg

In Chapter 3, we looked at the functional style of programming, which provides some interesting ways to write code, but doesn’t quite stand on its own. The purely functional style doesn’t integrate well with the existing .NET framework class libraries and sometimes requires complicated solutions for simple problems.

In this chapter, you learned how to update values, which enables you to write new types of programs. Now you can use efficient collections to store program results, loop as necessary, and should any problems occur, throw exceptions.

Now you can make a choice as to how to approach problems, and you can begin to see the value of multiparadigm computing. Some problems can be solved by simply building up a mutable data structure, while others can be built up through combining simple functions to transform immutable data. You have options with F#.

In the next chapter, we will look at object-oriented programming. This third paradigm of F# doesn’t necessarily add much more computational power, but it does provide a way for programmers to organize and abstract code. In much the same way that pure functions can be composed, so can objects.

The best content for your career. Discover unlimited learning on demand for around $1/day.