F# Exception Handling in Asynchronous Context
Marijana
Posted on April 3, 2022
⚠️ 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
Console output
Starting someAsyncFunction...
Result from someAsyncFunction
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
Console output
Starting someAsyncFunction...
someAsyncFunction threw Exception
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
Console output
Starting someAsyncFunction...
Starting someChildAsyncFunction...
someChildAsyncFunction raised Exception
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")
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)
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
Posted on April 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.