4

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

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
}
Bellarmine Head
  • 3,397
  • 2
  • 22
  • 31

2 Answers2

2

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.

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
  • Thanks for this: a lot of good ideas and food for thought here. – Bellarmine Head Dec 23 '17 at 10:14
  • `This illustrates the preferred F# way of dealing with exceptions: avoid them, never throw them` - there seems to be some disagreement on this. e.g. in [this example.](https://eiriktsarpalis.wordpress.com/2017/02/19/youre-better-off-using-exceptions/) [Official guidance](http://fsharp.org/specs/component-design-guidelines/#34-exceptions) seems to be more equivocal. – Bellarmine Head Dec 23 '17 at 10:22
  • Your link to the official guidance is broken but the current version is in line with @fyodorSoikin point https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions#error-management – John Zabroski Nov 24 '22 at 04:29
2

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.

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • I've used `async` before and had no problems with it - for exactly the same `HttpClient` calls that I'm using now (not shown in my samples of course). But I recently read about this new `task` CE via the Giraffe framework, and how it might be a better fit for working with .NET Tasks. So I thought I'd give it a spin. – Bellarmine Head Dec 23 '17 at 10:04
  • 1
    There's so much I love about F#: domain modelling with record types, DUs, exhaustive pattern matching, and much more. But this business of F# being fundamentally expression-based, the lack of early return, not being able to express: do *this* statement and then *that* statement (vertically arranged, like in the C# example) slays me every time. My perfect language would have all these wonderful FP constructs like DUs and pattern matching, with statement sequences and early return. So far the only language I've come across that ticks all these boxes is Rust. – Bellarmine Head Dec 23 '17 at 10:12