Reputation: 3647
Given the F# task
computation expression I can write:-
task {
try
let! accessToken = getAccessTokenAsync a b
try
let! resource = getResourceAsync accessToken uri
// do stuff
with
| ex -> printfn "Failed to get API resource. %s" ex.Message
with
| ex -> printfn "Failed to get access token. %s" ex.Message
return ()
}
but what I want to do is have non-nested exception handling around the two getBlahAsync
function calls. This can be done in C# quite easily in an async
method with multiple await
s.
How to do so in an F# computation expression? If I try it in the simple and obvious way, accessToken
from the first try..with
doesn't flow into the second try..with
.
(The trouble with nesting is that the // do stuff
section could grow a bit, pushing the outer with
further and further away from its try
.)
How to do it in C#:-
static async Task MainAsync()
{
String accessToken = null;
try
{
accessToken = await GetAccessTokenAsync("e", "p");
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to get access token. " + ex.Message);
return;
}
String resource = null;
try
{
resource = await GetResourceAsync(accessToken);
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to get API resource. " + ex.Message);
return;
}
// do stuff
}
Upvotes: 4
Views: 486
Reputation: 243126
The main problem with translating the C# code is that F# does not let you use return
to jump out of the function body early. You can avoid nesting exceptions in various ways, but you will not be able to return early. This can be implemented as another computatione expression, but that's more of a curiosity than something you'd actually want to use here.
My recommendation would be to just split the function into one that gets all the resources and handles exceptions and another one that does the stuff. That does not eliminate nesting, but it will make the code fairly readable.
let doStuff accessToken resource = task {
// do stuff
}
let getResourcesAndDoStuff a b uri = task {
try
let! accessToken = getAccessTokenAsync a b
try
let! resource = getResourceAsync accessToken uri
return! doStuff accessToken resource
with ex ->
printfn "Failed to get API resource. %s" ex.Message
with ex ->
printfn "Failed to get access token. %s" ex.Message
}
As an aside, do you have some particular reason for using task
rather than the normal built-in F# async
workflow? It is not necessarily a problem, but async
composes better and supports cancellation, so it is often a sensible default choice.
Upvotes: 2
Reputation: 80915
After your edit, I see that what you actually want is "early return" - an ability to "interrupt" the flow of execution before reaching the end point. This is generally not possible in F# (though some computation builders might offer specialized facilities for that), because F# is fundamentally expression-based, not statement-based.
A lack of early return is a good thing, because it forces you to think through carefully what your program is supposed to do, as opposed to just bailing. But that is a philosophical discussion for another time.
However, there are other ways of achieving a similar effect. In this specific case, I would put the two operations, together with their exception handling, into separate functions, then chain those functions together:
task {
let token = task {
try
let! t = getAccessTokenAsync a b
return Some t
with
| ex -> printfn "Failed to get access token. %s" ex.Message
return None
}
let resouce t = task {
try
let! r = getResourceAsync accessToken uri
// do stuff
with
| ex -> printfn "Failed to get API resource. %s" ex.Message
}
let! t = token
match t with
| None -> return ()
| Some token -> do! resource token
}
If you find yourself facing similar issues regularly, you may want to invest in a few helper functions that wrap exception handling and Option
chaining:
// Applies given Task-returning function to the given Option value,
// if the Option value is None, returns None again.
// This is essentially Option.map wrapped in a task.
let (<*>) f x = task {
match x with
| None -> return None
| Some r -> let! r' = f r
return Some r'
}
// Executes given Option-returning task, returns None if an exception was thrown.
let try'' errMsg f = task {
try return! f
with ex ->
printfn "%s %s" errMsg ex.Message
return None
}
// Executes given task, returns its result wrapped in Some,
// or returns None if an exception was thrown.
let try' errMsg f = try'' errMsg <| task { let! r = f
return Some r }
task {
let! token = getAccessTokenAsync a b |> try' "Failed to get access token."
let! resource = getResourceAsync uri <*> token |> try'' "Failed to get API resource."
do! doStuff <*> resource
}
This illustrates the preferred F# way of dealing with exceptions: avoid them, never throw them, instead return error types (the example above uses Option<_>
, but also see e.g. Result<_,_>
), and if you must interact with library code that does throw exceptions, put them inside wrappers that convert exceptions to error types.
Upvotes: 2