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
alternate version calls
takes a format string similar to
> // Using failwithf let divide x y = if y = 0 then failwithf
"Cannot divide %d by zero!" xx / 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
> // 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
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.
To handle an exception you catch it using a
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.
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,
:? 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 =
trylet filePath = args. printfn "Trying to gather information about file:" printfn "%s" filePath // Does the drive exist? let matchingDrive = Directory.GetLogicalDrives() |> Array.tryFind (fun drivePath -> drivePath. = filePath.) 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:
expressions. In a
expression, the code in the
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
Example 4-15. try-finally expressions
> // Try-finally expressions let tryFinallyTest() =
tryprintfn "Before exception..." failwith "ERROR!" printfn "After exception raised..."
finallyprintfn "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 = ()
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
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
:?, which we saw in previous
Example 4-17. Lightweight F# exception syntax
open System open System.Collections.Generic
exception NoFullMoon of int * int
exception BadMojo of stringlet castHex (ingredients : HashSet<string>) = try let currentWand = Environment.MagicWand if currentWand = null then
raise NoMagicWandif 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.