Chapter 15
Exceptions & Debugging
IntroductionExceptions are erroneous or unexpected conditions that arise during the execution of a program. Common exceptions include memory exhaustion, file permission errors, erroneous mathematical calculations, e.g., divide by zero errors, network timeouts, out-of-bounds indexing, accessing a null object, etc. When these types of problems occur, we can use exception handling to deal with them in a standard, structured manner. In this chapter, we will look at how to create, propagate and deal with exceptions. We will also discuss some of the diagnostic and defensive programming tools that F# provides. ExceptionsF# supports two types of exceptions: .NET-based exceptions and F#-based exceptions. The first part of this chapter assumes the code is working with .NET-based exceptions, the most common scenario. In the second part of this chapter, we explore F#-based exceptions. Handling .Net ExceptionsSince exceptions are the standard way to deal with erroneous and unexpected conditions in .NET, it should come as no surprise that .NET ships with dozens of predefined exception classes that cover all manner of problems. Some of these classes include: ApplicationException, FileNotFoundException, EndOfStreamException, InvalidCastException, etc. In F#, by default, when an exception occurs, the runtime creates an exception object (an instance of a particular exception class) that describes the error in detail. The runtime then begins the process of walking the call stack looking for an exception handler – a block of code that has declared itself to be a handler for this type of exception. In exception parlance, we say that the runtime has created and then “thrown” an exception. If the runtime finds a handler capable of processing the type of exception thrown, we say the handler “catches” the exception. It is normally your responsibility to provide a handler to catch an exception, unless you want your program to terminate using the default runtime handler. If the runtime finds the right kind of handler in the call stack of your code, it will pass it the exception object to it. Your handler can then do whatever it likes with the exception, including actions that try to fix the problem, log an error, pass the exception further up the stack, ignore the exception, etc. The runtime stops walking up the stack when it finds a suitable handler. A suitable handler is a try…with expression surrounding calling code on the execution stack. If no suitable handler is found, F# will eventually display an error message and halt the program. try…withTo handle exceptions in F#, we use try…with to wrap an expression where an exception may occur. try..with has the following general syntax: try When we sandwich an expression in try…with, we’re telling F# that we want to know about exceptions that occur within that expression. The try…with sets up an exception frame – a point in the code that’s indicating to F# its interest in exceptions that meet the patterns listed in the with part. When an exception is caught, it is matched against the patterns in the with block, much as if we had a match expression where the exception instance was the argument. A try…with expression evaluates to the last line of the expression between try and with, or to whatever exception handler expression is executed. This means that the expression between try and with, must be compatible with the expression types in the with block, otherwise, the F# compiler will complain that the over try…with expression is inconsistent. It’s important to note that try blocks introduce their own scopes, meaning that values declared within try blocks cannot be used in the associated with blocks, since they are out of scope. The try block applies to any call within its call chain. When an exception occurs anywhere within the chain, the runtime will jump to the associated with clause and begin matching the thrown exception object against the patterns specified in the with block. The primary expression code (between try and with) will not complete its execution when an exception is thrown during expression evaluation. In general, we implement the with block by pattern matching upcasted types of the exception’s type via the :? operator. As soon as the matching engine finds a successful pattern match, it executes the expression associated with the pattern (expression2 or expression3 in the above syntax description). If the matching engine cannot find a match, the runtime continues walking up the call stack until it either finds a suitable handler or exhausts the stack. In the following code, we show a representative example of throwing and catching an exception: let div x y = x / y
try let a = div 5 0 printfn "%A" a with | :? System.DivideByZeroException -> printfn "You cannot divide by zero!" When implementing functions that may throw exceptions, it is a common style to generate option (Some/None) return values. This makes sense, since the function may not be able to return a meaningful result. Using this style, we would rewrite the div example as follows: let div x y = try Some(x / y) with | :? System.DivideByZeroException -> printfn "Cannot divide by zero!"; None
div 10 2 div 19 0 As with all pattern matching, F# will execute the first match that it finds, ignoring all the others. This makes the ordering of exception handlers important, especially since we’re comparing against class type. All “handle-able” exceptions eventually derived from System.Exception. So, if you match to System.Exception, that should be the last exception type listed in the handlers. The rule of thumb is to order your handler from most-specific to least-specific, i.e., specific to general. For example, if we were to write the previous example like this… let div x y = try Some(x / y) with | :? System.Exception -> printfn "Always handle exceptions here!"; None | :? System.DivideByZeroException -> printfn "Cannot divide by zero!"; None
…we’d have a problem, since the DivideByZeroException would never execute. It would always be superceded by the previous pattern System.Exception. Note in this example the use of the semicolon used to separate the printfn and the return value (None). We can use a semicolon to put several expressions on the same line. try…finallySometimes when an exception occurs, our application may have resources that it needs to release or half-completed work that it needs to undo. In these cases, using try…with can lead to resources leaks and inconsistent state, since all expressions following the offending one are skipped. This makes cleanup difficult. So, if you have cleanup to do, whether or not an exception occurs, use try…finally. The syntax is as follows: try The code in the finally block (expression2..m above) will always execute, regardless of whether executing expression1..n generates an exception. If the code in the try block generates an exception, F# transfers control to the finally block, then to the next matching exception handler up the call stack, if one exists. The following code demonstrates the use of try…finally to clean up a graphics resource: open System.Drawing let brush = new SolidBrush(Color.Blue) try printfn "About to 'paint' and throw an exception..." let result = 10 / 0 printfn "'Paint' complete" // should never be executed finally printfn "Cleaning up brush" brush.Dispose()
With try…finally blocks, the last expression of the try block is considered to be the expression’s return type. The finally expression does not contribute to the return type of the expression. Combining try...with and try…finallyOften in real-world programs we want to combine the following: executing potentially exceptional code, catching exceptions that occur, and performing come cleanup. F# does not have a try…with…finally construct, but instead favors nesting try…with blocks inside try…finally blocks, producing a similar effect. In the following example, we see how to use try…with combined with try…finally: open System open System.IO
let mutable f: FileStream = null // so that all blocks can see it, // we define it outside all trys try printfn "Opening file" f <- new FileStream(@"c:\temp\badfile.bin", FileMode.Open) try Some(f.ReadByte()) with | :? FileNotFoundException -> printfn "Cannot find input file"; None | :? Exception -> printfn "Unspecified exception"; None finally printfn "Closing file" if f <> null then f.Close() Creating F# ExceptionsIn F#, you are not limited to using predefined .NET-based exceptions. You can also define your own F#-based exceptions using the following syntax: exception exception-type of argument-type The keyword exception introduces a new exception type. The exception-type is the name of the exception “class”, and argument-type is a tuple of field types making up the contents of the exception. type. Let’s take a look at several examples to help illustrate the idea: exception LightweightException of string exception AccountOverdrawnException of string * decimal
Given the definition of these two F# exceptions, let’s see how we can use them in code: try // some bad statement () with | LightweightException(arg) -> printfn "LightweightException handled" | AccountOverdrawnException(s, d) -> printfn "Account %s is overdrawn by %f" s d
When catching F# exceptions, we need to use different matching patterns than when catching .NET exceptions. With F# exceptions, we match on the exception type and its arguments, as shown above. Even though we need to use different patterns for catching .NET and F# exceptions, we can combine them in the same with block, as illustrated in the example below (the first two patterns match F# exceptions, the last two match .NET exceptions): exception LightweightException of string exception AccountOverdrawnException of string * decimal
try // some bad statement () with | LightweightException(arg) -> printfn "LightweightException handled" | AccountOverdrawnException(s, d) -> printfn "Account %s is overdrawn by %f" s d | :? System.IO.FileNotFoundException -> printfn "File not found" | :? System.Exception -> printfn "Exception of last resort" Throwing ExceptionsNot only can we create and handle .NET and F# exceptions, we can also throw or “raise” them explicitly from our code. We may want to do this to inform a caller that something went wrong, e.g., the caller passed in an invalid parameter, we could not perform a calculation, etc. There are two ways to generate exceptions: using raise and using failwith. raiseWe use the keyword raise to generate both .NET and F# exceptions. Using raise is quite simple, as demonstrated by the general syntax: raise exception Using raise causes the runtime to generate the specified exception and to start the standard stack unwinding and exception handling process. Here are a few examples: exception AccountOverdrawnException of string * decimal
try let t, f = true, false if t then raise(AccountOverdrawnException("123", 4567.0m)) if not f then raise(System.Exception("bad stuff happened")) with | AccountOverdrawnException(acct, amt) -> printfn "Account %s overdrawn by %f" acct amt | :? System.Exception as err -> printfn "%s" err.Message rethrowAs of this writing, F# supports the rethrow function, which enables us to re-raise an exception from within an exception handler. This enables our handler to process (or ignore) an exception and then “pass it up” the call stack so that other handlers can process it as well. You can use rethrow only from within an existing exception hander. The following code demonstrates how to use it: try let x = 10 Some(x) with | :? System.Exception as e -> printfn "%s" e.Message; rethrow() Note that rethrow does not take any parameters. It re-raises the exception present in the current context.
|
||
FeedbackWe welcome your feedback. If you have comments or questions about this chapter, please feel free to e-mail us at Keep Reading |