Exceptions
Syntax for throwing and catching
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:
That's it! Defining new exception classes is a lot easier than in C#!
Throwing exceptions
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#:
failwith
throws a genericSystem.Exception
invalidArg
throws anArgumentException
nullArg
throws aNullArgumentException
invalidOp
throws anInvalidOperationException
These four probably cover most of the exceptions you would regularly throw. Here is how they are used:
By the way, there's a very useful variant of failwith
called failwithf
that includes printf
style formatting, so that you can make custom messages easily:
Throwing exceptions, method 2: using one of the standard .NET exception classes
You can raise
any .NET exception explicitly:
Throwing exceptions, method 3: using your own F# exception types
Finally, you can use your own types, as defined earlier.
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.
Question: what do you think the function signature will be if both branches raise exceptions?
Try it and see!
Catching exceptions
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.
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:
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:
When we try to evaluate it, we get an error:
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.
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.
Rethrowing exceptions
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# throw
keyword.
Try-finally
Another familiar expression is try-finally
. As you might expect, the "finally" clause will be called no matter what.
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 string
.
The "finally" clause must always return unit, so any non-unit values will be flagged by the compiler.
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.
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:
Note the use of Some
and 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.
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.
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
module:
List.find
will throw aKeyNotFoundException
if the key is not foundBut
List.tryFind
will return an Option type, withNone
if 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
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 tryDivide
or 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.
The client is then forced to handle the common cases, while uncommon exceptions will be caught by a handler higher up the call chain.
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:
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 ignore
)
Last updated
Was this helpful?