Just like other .NET languages, F# supports throwing and catching exceptions. As with the control flow expressions, the syntax will feel familiar, but again there are a few catches that you should know about.
Defining your own exceptions
When raising/throwing exceptions, you can use the standard system ones such as
InvalidOperationException, or you can define your own exception types using the simple syntax shown below, where the "content" of the exception is any F# type:
exception MyFSharpError1 of string exception MyFSharpError2 of string * int
That's it! Defining new exception classes is a lot easier than in C#!
There are three basic ways to throw an exception
- Using one of the built in functions, such as "invalidArg"
- Using one of the standard .NET exception classes
- Using your own custom exception types
Throwing exceptions, method 1: using one of the built in functions
There are four useful exception keywords built into F#:
failwiththrows a generic
These four probably cover most of the exceptions you would regularly throw. Here is how they are used:
// throws a generic System.Exception let f x = if x then "ok" else failwith "message" // throws an ArgumentException let f x = if x then "ok" else invalidArg "paramName" "message" // throws a NullArgumentException let f x = if x then "ok" else nullArg "paramName" "message" // throws an InvalidOperationException let f x = if x then "ok" else invalidOp "message"
By the way, there's a very useful variant of
failwithf that includes
printf style formatting, so that you can make custom messages easily:
open System let f x = if x = "bad" then failwithf "Operation '%s' failed at time %O" x DateTime.Now else printfn "Operation '%s' succeeded at time %O" x DateTime.Now // test f "good" f "bad"
Throwing exceptions, method 2: using one of the standard .NET exception classes
raise any .NET exception explicitly:
// you control the exception type let f x = if x then "ok" else raise (new InvalidOperationException("message"))
Throwing exceptions, method 3: using your own F# exception types
Finally, you can use your own types, as defined earlier.
// using your own F# exception types let f x = if x then "ok" else raise (MyFSharpError1 "message")
And that's pretty much it for throwing exceptions.
What effect does raising an exception have on the function type?
We said earlier that both branches of an if-then-else expression must return the same type. But how can raising an exception work with this constraint?
The answer is that any code that raises exceptions is ignored for the purposes of determining expression types. This means that the function signature will be based on the normal case only, not the exception case.
For example, in the code below, the exceptions are ignored, and the overall function has signature
bool->int, as you would expect.
let f x = if x then 42 elif true then failwith "message" else invalidArg "paramName" "message"
Question: what do you think the function signature will be if both branches raise exceptions?
let f x = if x then failwith "error in true branch" else failwith "error in false branch"
Try it and see!
Exceptions are caught using a try-catch block, as in other languages. F# calls it
try-with instead, and testing for each type of exception uses the standard pattern matching syntax.
try failwith "fail" with | Failure msg -> "caught: " + msg | MyFSharpError1 msg -> " MyFSharpError1: " + msg | :? System.InvalidOperationException as ex -> "unexpected"
If the exception to catch was thrown with
failwith (e.g. a System.Exception) or a custom F# exception, you can match using the simple tag approach shown above.
On the other hand, to catch a specific .NET exception class, you have to match using the more complicated syntax:
:? (exception class) as ex
Again, as with if-then-else and the loops, the try-with block is an expression that returns a value. This means that all branches of the
try-with expression must return the same type.
Consider this example:
let divide x y= try (x+1) / y // error here -- see below with | :? System.DivideByZeroException as ex -> printfn "%s" ex.Message
When we try to evaluate it, we get an error:
error FS0043: The type 'unit' does not match the type 'int'
The reason is that the "
with" branch is of type
unit, while the "
try" branch is of type
int. So the two branches are of incompatible types.
To fix this, we need to make the "
with" branch also return type
int. We can do this easily using the semicolon trick to chain expressions on one line.
let divide x y= try (x+1) / y with | :? System.DivideByZeroException as ex -> printfn "%s" ex.Message; 0 // added 0 here! //test divide 1 1 divide 1 0
Now that the
try-with expression has a defined type, the whole function can be assigned a type, namely
int -> int -> int, as expected.
As before, if any branch throws an exception, it doesn't count when types are being determined.
If needed, you can call the "
reraise()" function in a catch handler to propagate the same exception up the call chain. This is the same as the C#
let divide x y= try (x+1) / y with | :? System.DivideByZeroException as ex -> printfn "%s" ex.Message reraise() //test divide 1 1 divide 1 0
Another familiar expression is
try-finally. As you might expect, the "finally" clause will be called no matter what.
let f x = try if x then "ok" else failwith "fail" finally printf "this will always be printed"
The return type of the try-finally expression as a whole is always the same as return type of the "try" clause on its own. The "finally" clause has no effect on the type of the expression as a whole. So in the above example, the whole expression has type
The "finally" clause must always return unit, so any non-unit values will be flagged by the compiler.
let f x = try if x then "ok" else failwith "fail" finally 1+1 // This expression should have type 'unit
Combining try-with and try-finally
The try-with and the try-finally expressions are distinct and cannot be combined directly into a single expression. Instead, you will have to nest them as circumstances require.
let divide x y= try try (x+1) / y finally printf "this will always be printed" with | :? System.DivideByZeroException as ex -> printfn "%s" ex.Message; 0
Should functions throw exceptions or return error structures?
When you are designing a function, should you throw exceptions, or return structures which encode the error? This section will discuss two different approaches.
The pair of functions approach
One approach is to provide two functions: one which assumes everything works and throws an exception otherwise and a second "tryXXX" function that returns a missing value if something goes wrong.
For example, we might want to design two distinct library functions for division, one that doesn't handle exceptions and one that does:
// library function that doesn't handle exceptions let divideExn x y = x / y // library function that converts exceptions to None let tryDivide x y = try Some (x / y) with | :? System.DivideByZeroException -> None // return missing
Note the use of
None Option types in the
tryDivide code to signal to the client whether the value is valid.
With the first function, the client code must handle the exception explicitly.
// client code must handle exceptions explicitly try let n = divideExn 1 0 printfn "result is %i" n with | :? System.DivideByZeroException as ex -> printfn "divide by zero"
Note that there is no constraint that forces the client to do this, so this approach can be a source of errors.
With the second function the client code is simpler, and the client is constrained to handle both the normal case and the error case.
// client code must test both cases match tryDivide 1 0 with | Some n -> printfn "result is %i" n | None -> printfn "divide by zero"
This "normal vs. try" approach is very common in the .NET BCL, and also occurs in a few cases in the F# libraries too. For example, in the
List.findwill throw a
KeyNotFoundExceptionif the key is not found
List.tryFindwill return an Option type, with
Noneif the key is not found
If you are going to use this approach, do have a naming convention. For example:
- "doSomethingExn" for functions that expect clients to catch exceptions.
- "tryDoSomething" for functions that handle normal exceptions for you.
Note that I prefer to have an "Exn" suffix on "doSomething" rather than no suffix at all. It makes it clear that you expect clients to catch exceptions even in normal cases.
The overall problem with this approach is that you have to do extra work to create pairs of functions, and you reduce the safety of the system by relying on the client to catch exceptions if they use the unsafe version of the function.
The error-code-based approach
"Writing good error-code-based code is hard, but writing good exception-based code is really hard." Raymond Chen
In the functional world, returning error codes (or rather error types) is generally preferred to throwing exceptions, and so a standard hybrid approach is to encode the common cases (the ones that you would expect a user to care about) into a error type, but leave the very unusual exceptions alone.
Often, the simplest approach is just to use the option type:
Some for success and
None for errors. If the error case is obvious, as in
tryParse, there is no need to be explicit with more detailed error cases.
But sometimes there is more than one possible error, and each should be handled differently. In this case, a union type with a case for each error is useful.
In the following example, we want to execute a SqlCommand. Three very common error cases are login errors, constraint errors and foreign key errors, so we build them into the result structure. All other errors are raised as exceptions.
open System.Data.SqlClient type NonQueryResult = | Success of int | LoginError of SqlException | ConstraintError of SqlException | ForeignKeyError of SqlException let executeNonQuery (sqlCommmand:SqlCommand) = try use sqlConnection = new SqlConnection("myconnection") sqlCommmand.Connection <- sqlConnection let result = sqlCommmand.ExecuteNonQuery() Success result with | :?SqlException as ex -> // if a SqlException match ex.Number with | 18456 -> // login Failed LoginError ex | 2601 | 2627 -> // handle constraint error ConstraintError ex | 547 -> // handle FK error ForeignKeyError ex | _ -> // don't handle any other cases reraise() // all non SqlExceptions are thrown normally
The client is then forced to handle the common cases, while uncommon exceptions will be caught by a handler higher up the call chain.
let myCmd = new SqlCommand("DELETE Product WHERE ProductId=1") let result = executeNonQuery myCmd match result with | Success n -> printfn "success" | LoginError ex -> printfn "LoginError: %s" ex.Message | ConstraintError ex -> printfn "ConstraintError: %s" ex.Message | ForeignKeyError ex -> printfn "ForeignKeyError: %s" ex.Message
Unlike a traditional error code approach, the caller of the function does not have to handle any errors immediately, and can simply pass the structure around until it gets to someone who knows how to handle it, as shown below:
let lowLevelFunction commandString = let myCmd = new SqlCommand(commandString) executeNonQuery myCmd //returns result let deleteProduct id = let commandString = sprintf "DELETE Product WHERE ProductId=%i" id lowLevelFunction commandString //returns without handling errors let presentationLayerFunction = let result = deleteProduct 1 match result with | Success n -> printfn "success" | errorCase -> printfn "error %A" errorCase
On the other hand, unlike C#, the result of a expression cannot be accidentally thrown away. So if a function returns an error result, the caller must handle it (unless it really wants to be badly behaved and send it to
let presentationLayerFunction = do deleteProduct 1 // error: throwing away a result code!