Part of the "Expressions and syntax" series (link)

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:

exception MyFSharpError1 of string
exception MyFSharpError2 of string * int

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 generic System.Exception
  • invalidArg throws an ArgumentException
  • nullArg throws a NullArgumentException
  • invalidOp throws an InvalidOperationException

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 failwith called 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

You can 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!

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.

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.

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.

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

Try-finally

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 string.

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 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.

// 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 module:

  • List.find will throw a KeyNotFoundException if the key is not found
  • But List.tryFind will return an Option type, with None 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

“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 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.

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 ignore)

let presentationLayerFunction =
  do deleteProduct 1    // error: throwing away a result code!

Comments

blog comments powered by Disqus