seguso
seguso

Reputation: 2273

How to catch exception thrown by Task run with Async.AwaitTask

Edit: This turned out to be an F# bug which can be worked-around by using a custom option type instead of Fsharp's "Option".

In F#, I am trying to call a .net Task with Async.AwaitTask. The task is throwing an exception, and I can't seem to catch it with either try-catch or Async.Catch. And I know of no other way to catch exceptions. What is the solution? And what is the cause of the problem? Thanks for any explanation.

Here is some test code that shows my failure to catch the exception thrown by DownloadStringTaskAsync:

open System
open System.Net

[<EntryPoint>]
let main argv = 

    let test =
        async{
        let! exc = Async.Catch( async{
            try
                let w = new Net.WebClient();
                let! str = Async.AwaitTask (w.DownloadStringTaskAsync "") // throws ArgumentException
                return Some str
            with 
            | _ -> 
                return None // not caught
            }
            )
        match exc with
        | Choice1Of2 r -> return r
        | Choice2Of2 ext -> return None  // not caught
        }

    let res = Async.RunSynchronously(test)
    let str = Console.ReadLine();
    0 // return an integer exit code

I need some way to catch the exception inside the "test" async function, preventing it to "bubble up".

I found this related question, but I can't seem to adapt the answer (which deals with Task, not with Task<'a>) to my needs.

Edit: Here is a screenshot showing the problem: https://i.gyazo.com/883f546c00255b210e53cd095b876cb0.png

"ArgumentException was unhandled by user code."

(Visual Studio 2013, .NET 4.5.1, Fsharp 3.1, Fsharp.core.dll 4.3.1.0.)

Could it be that the code is correct but some Fsharp or Visual Studio setting is preventing the exception from being caught?

Upvotes: 5

Views: 1787

Answers (1)

Alex Hardwicke
Alex Hardwicke

Reputation: 541

TL;DR: The exception does get caught, it appears to be returning None out of async that's confusing here - the result is null, not None, which is screwing up pattern matching and generally being a bother. Use your own union type instead of Option to handle it.

Edit: Oh, and @takemyoxygen's comments about why you're seeing it straight away is correct. Debug->Exceptions, or just untick "Break when this exception type is user-unhandled" in the pop-up visible in your screenshot. It looks like VS is breaking on THROW, rather than actually on unhandled.

First, returning Some/None inside an Async.Catch makes it useless. Async.Catch returns Choice1Of2(val) unless there's an uncaught exception, so the code as is (if it worked) would return Choice1Of2(Some(string)) with no exception, or Choice1Of2(None) with exception. Either use try/with and handle the exception yourself, or only use Async.Catch.

Experiments:

Proof that we are catching: Add a debug output to the "with". The code DOES get there (add a breakpoint or look at debug output for proof). We get Choice1Of2(null), rather than Choice1Of2(None) in exc though. WEIRD. See image: https://i.sstatic.net/21sfH.png

open System
open System.Net

[<EntryPoint>]
let main argv = 

    let test =
        async{
        let! exc = Async.Catch( async{
            try
                let w = new Net.WebClient();
                let! str = Async.AwaitTask (w.DownloadStringTaskAsync "") // throws ArgumentException
                return Some str
            with 
            | _ -> 
                System.Diagnostics.Debug.WriteLine "in with" // We get here.
                return None // not caught
            }
            )
        match exc with
        | Choice1Of2 r -> return r
        | Choice2Of2 ext -> return None  // not caught
        }

    let res = Async.RunSynchronously(test)
    let str = Console.ReadLine();
    0 // return an integer exit code

Remove Async.Catch: Don't use Async.Catch (keep the debug output though). Again, we get to the debug output, so we're catching the exception, and as we're not wrapping in a Choice from Async.Catch, we get null instead of None. STILL WEIRD.

open System
open System.Net

[<EntryPoint>]
let main argv = 

    let test = async {
        try
            let w = new Net.WebClient();
            let! str = Async.AwaitTask (w.DownloadStringTaskAsync "") // throws ArgumentException
            return Some str
        with 
        | _ -> 
            System.Diagnostics.Debug.WriteLine "in with"
            return None }

    let res = Async.RunSynchronously(test)
    let str = Console.ReadLine();
    0 // return an integer exit code

Only use Async.Catch: Don't use a try/with, just Async.Catch. This works a bit better. At the pattern match on exc, I have a Choice2Of2 containing the exception. However, when the None is returned out of the outermost async, it becomes null.

open System
open System.Net

[<EntryPoint>]
let main argv = 

    let test = async {
        let! exc = Async.Catch(async {
            let w = new Net.WebClient();
            let! str = Async.AwaitTask (w.DownloadStringTaskAsync "") // throws ArgumentException
            return str })
            
       match exc with      
       | Choice1Of2 v -> return Some v
       | Choice2Of2 ex -> return None  
       }

    let res = Async.RunSynchronously(test)
    let str = Console.ReadLine();
    0 // return an integer exit code

Don't use Option: Interestingly, if you use a custom union type, it works perfectly:

open System
open System.Net

type UnionDemo =
    | StringValue of string
    | ExceptionValue of Exception

[<EntryPoint>]
let main argv = 

    let test = async {
        let! exc = Async.Catch(async {
            let w = new Net.WebClient();
            let! str = Async.AwaitTask (w.DownloadStringTaskAsync "") // throws ArgumentException
            return str })
            
       match exc with      
       | Choice1Of2 v -> return StringValue v
       | Choice2Of2 ex -> return ExceptionValue ex 
       }

    let res = Async.RunSynchronously(test)
    let str = Console.ReadLine();
    0 // return an integer exit code

Using a try/with and no Async.Catch also works with a new Union:

open System
open System.Net

type UnionDemo =
    | StringValue of string
    | ExceptionValue of Exception

[<EntryPoint>]
let main argv = 

    let test = async {
        try
            let w = new Net.WebClient();
            let! str = Async.AwaitTask (w.DownloadStringTaskAsync "") // throws ArgumentException
            return StringValue str
        with
        | ex -> return ExceptionValue ex }

    let res = Async.RunSynchronously(test)
    let str = Console.ReadLine();
    0 // return an integer exit code

It works even if the union is defined as the following:

type UnionDemo =
    | StringValue of string
    | ExceptionValue

The following gets null in moreTestRes.

[<EntryPoint>]
let main argv = 

    let moreTest = async {
        return None
    }

    let moreTestRes = Async.RunSynchronously moreTest
    0

Why this is the case I don't know (still happens in F# 4.0), but the exception is definitely being caught, but None->null is screwing it up.

Upvotes: 5

Related Questions