Bellarmine Head
Bellarmine Head

Reputation: 3647

How to implement non-nested exception handling on each step in an F# task computation expression?

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
}

Upvotes: 4

Views: 486

Answers (2)

Tomas Petricek
Tomas Petricek

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

Fyodor Soikin
Fyodor Soikin

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

Related Questions