Skip to content

Instantly share code, notes, and snippets.

@jwosty
Created November 15, 2023 19:55
Show Gist options
  • Save jwosty/66713eae687cb026d8e2222dd0cc8cf3 to your computer and use it in GitHub Desktop.
Save jwosty/66713eae687cb026d8e2222dd0cc8cf3 to your computer and use it in GitHub Desktop.
open System
open System.Diagnostics
open System.IO
open System.Runtime.ExceptionServices
module ThirdPartyLibrary =
// Represents some low-level async "primitive" which is used in many many places and which may fail. This type of
// method/function is not typically what you're interested in when examining stacktraces.
// Example: a database query function from a thid-party library
let doWorkAsync crash = ExtraTopLevelOperators.async {
do! Async.Sleep 100
if crash then
raise (IOException("Oh no!"))
}
// Helper function to improve stacktraces. This is the recommended way to augment (not replace) exception stacktraces
// with the caller's stack frame information
// See: https://stackoverflow.com/a/17091351/1231925
let inline rethrowAsync (computation: Async<'a>) = async {
try
return! computation
with e ->
let s = StackTrace()
Debug.WriteLine $"StackTrace: %O{s}"
Debug.WriteLine $"Rethrowing: %s{e.StackTrace}"
Debug.WriteLine ""
ExceptionDispatchInfo.Throw e
return Unchecked.defaultof<_> // unreachable, but the compiler doesn't realize that
}
// Application function. We need this to show up in stack traces
let doLoopAsync (n: int) = async {
for n in n .. -1 .. 0 do
printfn "n = %d" n
let crash = n = 6
do! rethrowAsync (ThirdPartyLibrary.doWorkAsync crash)
}
// Higher level application function. We also need this to show up in stacktraces
let mainAsync () = async {
let n = 10
printfn "Starting with iterations: %d" n
do! rethrowAsync (doLoopAsync n)
printfn "Done."
}
[<EntryPoint>]
let main _ =
Async.RunSynchronously (mainAsync ())
// Even when using the suggested workarounds to recapture stack trace information by rethrowing, the stacktrace we
// get is completely useless:
//
// Unhandled exception. System.IO.IOException: Oh no!
// at Program.ThirdPartyLibrary.doWorkAsync@12-1.Invoke(Unit _arg1) in C:\Users\jwostenberg\Documents\Code\FSharpSandbox\ConsoleApp2\Program.fs:line 13
// at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 525
// at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112
// --- End of stack trace from previous location ---
// at Microsoft.FSharp.Control.AsyncResult`1.Commit() in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 453
// at Microsoft.FSharp.Control.AsyncPrimitives.QueueAsyncAndWaitForResultSynchronously[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 1133
// at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronously[T](CancellationToken cancellationToken, FSharpAsync`1 computation, FSharpOption`1 timeout) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 1160
// at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 1504
// at Program.main(String[] _arg1) in C:\Users\jwostenberg\Documents\Code\FSharpSandbox\ConsoleApp2\Program.fs:line 49
//
// Question of the day: what blew up? ¯\_(ツ)_/¯ Everything useful (doLoopAsync and mainAsync) still got lost, so
// there's no way to know. It's especially confounding since the rethrow helper is *supposed to work*, and indeed
// the debug print line shows that it appears to! I'm even fairly suspicious that this used to work at some point.
//
//
// Bonus points -- take this snippet:
// Async.RunSynchronously (async {
// try return! mainAsync ()
// with e -> eprintfn "%O" e
// })
// Somehow, this actually does show the complete stacktrace:
//
// System.IO.IOException: Oh no!
// at Program.ThirdPartyLibrary.doWorkAsync@12-1.Invoke(Unit _arg1) in C:\Users\jwostenberg\Documents\Code\FSharpSandbox\ConsoleApp2\Program.fs:line 13
// at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 525
// at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112
// --- End of stack trace from previous location ---
// at Program.doLoopAsync@36-4.Invoke(Exception _arg1)
// at Program.doLoopAsync@36-7.Invoke(Exception exn)
// at Microsoft.FSharp.Control.AsyncPrimitives.CallFilterThenInvoke[T](AsyncActivation`1 ctxt, FSharpFunc`2 filterFunction, ExceptionDispatchInfo edi) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 543
// at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112
// --- End of stack trace from previous location ---
// at Program.mainAsync@43-3.Invoke(Exception _arg1)
// at Program.mainAsync@43-6.Invoke(Exception exn)
// at Microsoft.FSharp.Control.AsyncPrimitives.CallFilterThenInvoke[T](AsyncActivation`1 ctxt, FSharpFunc`2 filterFunction, ExceptionDispatchInfo edi) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 543
// at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112
0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment