knocte
knocte

Reputation: 17939

F#: Proper `try` blocks inside an async{} block?

To simplify my scenario, let's suppose I have this simple code:

let someCondition = false

let SomeFuncThatThrows () =
    async {
        if someCondition then
            raise <| InvalidOperationException()
        return 0
    }

let DoSomethingWithFoo (foo: int) =
    Console.WriteLine (foo.ToString())

let SomeWrapper () =
    async {
        let! foo = SomeFuncThatThrows()
        DoSomethingWithFoo foo
    }

[<EntryPoint>]
let main argv =
    Async.RunSynchronously (SomeWrapper ())
    0

When executing it, it obviously just prints "0". However, some day, circumstances change, and some external factor makes someCondition become true. To prevent the program to crash in this scenario, I want to handle the exception. Then for an F# newbie it's easy to change SomeWrapper adding a try-with block, which most people would think that works:

let SomeWrapper () =
    async {
        let! foo =
            try
                SomeFuncThatThrows()
            with
            | :? InvalidOperationException ->
                Console.Error.WriteLine "aborted"
                Environment.Exit 1
                failwith "unreachable"
        DoSomethingWithFoo foo
    }

However, this above doesn't work (the exception is still unhandled), because SomeFuncThatThrows returns a successful result: an Async<int> element. What throws an exception is the let! foo = bit because it awaits the async workload.

However, if you want to change SomeWrapper to fix the exception handling, many may think this is possible:

let SomeWrapper () =
    async {
        let foo =
            try
                let! fooAux = SomeFuncThatThrows()
                fooAux
            with
            | :? InvalidOperationException ->
                Console.Error.WriteLine "aborted"
                Environment.Exit 1
                failwith "unreachable"
        DoSomethingWithFoo foo
    }

But no, the compiler is not happy, as it signals the following error:

/.../Program.fs(17,17): Error FS0750: This construct may only be used within computation expressions (FS0750) (SomeProject)

Then, it seems the only way I could fix it is this way:

let SomeWrapper () =
    async {
        try
            let! foo = SomeFuncThatThrows()
            DoSomethingWithFoo foo
        with
        | :? InvalidOperationException ->
            Console.Error.WriteLine "aborted"
            Environment.Exit 1
            failwith "unreachable"
    }

However, I'm not 100% happy with this solution, because the try-with is too wide, as it also covers the call to DoSomethingWithFoo function, which I wanted to leave outside the try-with block. Any better way to fix this without writing non-idiomatic F#? Should I report the compiler error as a feature-request in Microsoft's F# GitHub repo?

Upvotes: 2

Views: 246

Answers (3)

Tomas Petricek
Tomas Petricek

Reputation: 243051

The answer from @nilekirk works and encodes directly the logic that you were looking for, but as you noted in the comments, it is a fairly complex syntactic structure - you need a nested async { .. } expression.

You could extract the nested async block into a separate function, which makes the code much more readable:

let SafeSomeFunc () = async {
    try
        return! SomeFuncThatThrows()
    with
    | :? InvalidOperationException ->
        Console.Error.WriteLine "aborted"
        Environment.Exit 1
        return failwith "unreachable"
}

let SomeWrapper2 () = async {
    let! foo = SafeSomeFunc ()            
    DoSomethingWithFoo foo
}

Here, we actually need to put some return value into the with branch.

Upvotes: 0

Nghia Bui
Nghia Bui

Reputation: 3784

Any better way to fix this without writing non-idiomatic F#?

In idiomatic F# and functional code, we try to get rid of using exceptions and side-effects as much as possible.

Environment.Exit is a big side-effect, don't use it.

If SomeFuncThatThrows() must be able to throw exception (because e.g., you cannot modify its source code). Then try to wrap it inside a safe function which returns an Option value and use this function instead.

Your whole code can be rewritten as:

let someCondition = true

let SomeFuncThatThrows () =
    async {
        if someCondition then
            raise <| InvalidOperationException()
        return 0
    }

let SomeFunc () =
    async {
        try
            let! foo = SomeFuncThatThrows()
            return Some foo
        with _ ->
            return None
    }

let DoSomethingWithFoo (foo: int) =
    Console.WriteLine (foo.ToString())

let SomeWrapper () =
    async {
        match! SomeFunc() with
        | Some foo -> DoSomethingWithFoo foo
        | None -> Console.Error.WriteLine "aborted"
    }

[<EntryPoint>]
let main argv =
    Async.RunSynchronously (SomeWrapper ())
    0

Upvotes: 1

nilekirk
nilekirk

Reputation: 2383

You can wrap the call to SomeFuncThatThrows in a new async that contains a try...with:

let SomeWrapper () =
    async {
        let! foo = 
            async {
                try
                    return! SomeFuncThatThrows()
                with
                | :? InvalidOperationException ->
                    Console.Error.WriteLine "aborted"
                    Environment.Exit 1
                    return failwith "unreachable"
            }
        DoSomethingWithFoo foo
    }

Upvotes: 2

Related Questions