Reputation: 105
I have an async block, within this block I call an async method from an external C# web service client library. This method call returns a data transfer object, or a custom exception of type ApiException
. Initially, my function looked like this:
type Msg =
| LoginSuccess
| LoginError
| ApiError
let authUserAsync (client: Client) model =
async {
do! Async.SwitchToThreadPool()
let loginParams = new LoginParamsDto(Username = "some username", Password = "some password")
try
// AuthenticateAsync may throw a custom ApiException.
let! loggedInAuthor = client.AuthenticateAsync loginParams |> Async.AwaitTask
// Do stuff with loggedInAuthor DTO...
return LoginSuccess
with
| :? ApiException as ex ->
let msg =
match ex.StatusCode with
| 404 -> LoginError
| _ -> ApiError
return msg
}
But I found that the ApiException
wasn't being caught. Further investigation revealed that the ApiException
was in fact the inner exception. So I changed my code to this:
type Msg =
| LoginSuccess
| LoginError
| ApiError
let authUserAsync (client: Client) model =
async {
do! Async.SwitchToThreadPool()
let loginParams = new LoginParamsDto(Username = "some username", Password = "some password")
try
// AuthenticateAsync may throw a custom ApiException.
let! loggedInAuthor = client.AuthenticateAsync loginParams |> Async.AwaitTask
// Do stuff with loggedInAuthor DTO...
return LoginSuccess
with
| baseExn ->
let msg =
match baseExn.InnerException with
| :? ApiException as e ->
match e.StatusCode with
| 404 -> LoginError
| _ -> ApiError
| otherExn -> raise otherExn
return msg
}
Which seems to work. But being new to F# I'm wondering if there is there a more elegant or idiomatic way to catch an inner exception in this kind of situation?
Upvotes: 1
Views: 354
Reputation: 12667
As an alternative, if you use F# exceptions, you can use the full range of pattern matching available for product types. Error handling code reads a lot more succinctly.
exception TimeoutException
exception ApiException of message: string * inner: exn
try
action ()
with
| ApiException (_, TimeoutException) ->
printfn "Timed out"
| ApiException (_, (:? ApplicationException as e)) ->
printfn "%A" e
Upvotes: 0
Reputation: 243041
The solution based on active patterns from Tarmil is very nice and I would use that if I wanted to catch the exception in multiple places across a file or project. However, if I wanted to do this in just one place, then I would probably not want to define a separate active pattern for it.
There is a somewhat nicer way of writing what you have using the when
clause in the try ... with
expression:
let authUserAsync (client: Client) model =
async {
try
// (...)
with baseExn when (baseExn.InnerException :? ApiException) ->
let e = baseExn :?> ApiException
match e.StatusCode with
| 404 -> return LoginError
| _ -> return ApiError }
This is somewhat repetitive because you have to do a type check using :?
and then again a cast using :?>
, but it is a bit nicer inline way of doing this.
Upvotes: 3
Reputation: 11362
I sometimes use an active pattern to catch the main or the inner exception:
let rec (|NestedApiException|_|) (e: exn) =
match e with
| null -> None
| :? ApiException as e -> Some e
| e -> (|NestedApiException|_|) e.InnerException
and then use it like this:
async {
try
...
with
| NestedApiException e ->
...
| otherExn ->
raise otherExn
}
Upvotes: 4