DEV Community

Marijana
Marijana

Posted on

F# Exception Handling in Asynchronous Context

⚠️ All of the observations and examples in this article are valid for .NET 5 and below.

When it comes to exception handling the fundamental difference between catching exceptions in synchronous and asynchronous context is syntax: try-with vs Async.Catch.

The next few examples will demonstrate this difference as well as the proper usage of exception handling within the asynchronous context.

Async.Catch

Async.Catch creates an asynchronous computation that enables catching exceptions within the asynchronous workflow. It does that by returning an Async<Choice<'T, exn>> where 'T is the result type of the
asynchronous workflow and exn is the exception thrown.

The values of this result can be extracted by pattern matching.

It is important to remember:

  • If the asynchronous workflow is completed successfully, meaning no exceptions were thrown, then a Choice1Of2 is returned as the result value.
  • If an exception occurs within the async workflow then a Choice2Of2 is returned with the raised exception.

Let's see how we can use Async.Catch in asynchronous workflows.

Example 1. How to handle an expected result

This example shows how to use Async.Catch when someAsyncFunction returns an expected result (as opposed to an exception).

Once someAsyncFunction is complete, the functionExec will perform pattern matching which will result in Choice1Of2 being returned and then printed to the console.


let someAsyncFunction(raiseException: bool) : Async<unit> =
    async {
        printfn ("Starting someAsyncFunction...")
        do! Async.Sleep(1000)
        if(raiseException) then
            raise (System.Exception("someAsyncFunction threw Exception"))
    }

let functionExec(raiseException: bool) : Async<string> =
    async{
        let! result = someAsyncFunction(raiseException) |> Async.Catch
        return match result with
                | Choice1Of2 _ -> "Result from someAsyncFunction"
                | Choice2Of2 ex -> ex.Message
    }

let main() =
    async{
        let! result = functionExec(false)
        printfn($"{result}")
    }

Async.Start(main())
Async.Sleep 1000 |> Async.RunSynchronously

Enter fullscreen mode Exit fullscreen mode

Console output

Starting someAsyncFunction...
Result from someAsyncFunction
Enter fullscreen mode Exit fullscreen mode

Example 2. How to handle exception result

This example shows how to use Async.Catch when someAsyncFunction throws an exception.

Once someAsyncFunction throws an exception functionExec will perform pattern matching which will result in Choice2Of2 (the exception) being returned and then printed to the console.


let someAsyncFunction(raiseException: bool) : Async<unit> =
    async {
        printfn ("Starting someAsyncFunction...")
        do! Async.Sleep(1000)
        if(raiseException) then
            raise (System.Exception("someAsyncFunction threw Exception"))
    }

let functionExec(raiseException: bool) : Async<string> =
    async{
        let! result = someAsyncFunction(raiseException) |> Async.Catch
        return match result with
                | Choice1Of2 _ -> "Result from someAsyncFunction"
                | Choice2Of2 ex -> ex.Message
    }

let main() =
    async{
        let! result = functionExec(true)
        printfn($"{result}")
    }

Async.Start(main())
Async.Sleep 1000 |> Async.RunSynchronously

Enter fullscreen mode Exit fullscreen mode

Console output

Starting someAsyncFunction...
someAsyncFunction threw Exception
Enter fullscreen mode Exit fullscreen mode

Example 3: Nested Functions

In the case of nested functions within the asynchronous context when the most inner child function throws an exception, it will immediately stop all of the executions of the outer functions (just like in other popular languages, like C#).
The next example will demonstrate how this looks like.


let someChildAsyncFunction(raiseException: bool) : Async<unit> =
    async{
        printfn("Starting someChildAsyncFunction...")
        do! Async.Sleep(1000)
        if(raiseException) then
            raise (System.Exception("someChildAsyncFunction raised Exception"))
    }

let someAsyncFunction(raiseException: bool) : Async<unit> =
    async {
        printfn ("Starting someAsyncFunction...")
        do! someChildAsyncFunction(raiseException)
        printfn ("Ending someAsyncFunction...")
    }

let functionExec(raiseException: bool) : Async<string> =
    async{
        let! result = someAsyncFunction(raiseException) |> Async.Catch
        return match result with
                | Choice1Of2 _ -> "Some result"
                | Choice2Of2 ex -> ex.Message
    }

let main() =
    async{
        let! result = functionExec(true)
        printfn($"{result}")
    }

Async.Start(main())
Async.Sleep 1000 |> Async.RunSynchronously

Enter fullscreen mode Exit fullscreen mode

Console output

Starting someAsyncFunction...
Starting someChildAsyncFunction...
someChildAsyncFunction raised Exception
Enter fullscreen mode Exit fullscreen mode

We can see from the console that once someChildAsyncFunction throws exception it also terminates further execution of its caller someAsyncFunction.

Try-With

Now that we've seen how the Async.Catch let's see what happens if we try to use try-with within the asynchronous context


let someAsyncFunction() : Async<unit> =
    async {
        printfn ("Starting someAsyncFunction...")
        do! Async.Sleep(1000)
        raise (System.Exception("someAsyncFunction threw Exception"))
    }

try
    Async.Start(someAsyncFunction())
with
    | Failure message -> printfn($"{message}")

printfn("Hello, this example will blow up")

Enter fullscreen mode Exit fullscreen mode

Console output

Starting someAsyncFunction...
Unhandled exception. System.Exception: someAsyncFunction threw Exception
   at FSI_0015.someAsyncFunction@5-49.Invoke(Unit _arg1)
   at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 464
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 139
--- End of stack trace from previous location ---
   at Microsoft.FSharp.Control.AsyncPrimitives.Start@1077-1.Invoke(ExceptionDispatchInfo edi)
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 139
   at <StartupCode$FSharp-Core>.$Async.Sleep@1603-3.Invoke(Object _arg2) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 1609
   at System.Threading.TimerQueueTimer.<>c.<.cctor>b__27_0(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.TimerQueueTimer.CallCallback(Boolean isThreadPool)
   at System.Threading.TimerQueueTimer.Fire(Boolean isThreadPool)
   at System.Threading.TimerQueue.FireNextTimers()
   at System.Threading.TimerQueue.AppDomainTimerCallback(Int32 id)

Process finished with exit code 134 (interrupted by signal 6: SIGABRT)

Enter fullscreen mode Exit fullscreen mode

The console output shows that by using try-with we're getting an unhandled exception. What happens here is that the exception is thrown in a thread pool where someAsyncFucntion is executed.

Unhandled exceptions in thread pool terminate the process never propagating to Async.Start and never reaching the try-with block. (Source: MSDN - Exceptions in managed threads & Stack Overflow).

References

Top comments (0)