F# Async Stack Traces 3.0 vs 4.0

Here at Jet we use F# for our back end microservices and a lot of asynchronous computation expression. These are wonderful when you want to write non-blocking, reactive code, but in F# 3.0 they came with a price: useless stack traces. Look at the following example:

  let projectToSql connectionString log version message skus =
    let updateSkuMappings data add = async { 
      failwith "Error"
      log "ConnectionString = %s; version = %s" connectionString version
    }
 
    match message with
    | "SkuGroupSourcesAdded" -> updateSkuMappings skus true
    | "SkuGroupSourcesRemoved" -> updateSkuMappings skus false
    | _ -> async { failwith "Error" };;  

With F# 3.0 the stack traces generated from exceptions thrown in this code were essentially useless.

Running:

  projectToSql "database.com" (fun text cs v -> 
    let textFormat = Printf.TextWriterFormat <string -> unit>(text)
    printfn textFormat cs v) "1.0" "SkuGroupSourcesAdded" ["12ikh412k4"; "1k2jh412kj4h12"] 
  |> Async.RunSynchronously;;

gave you:

System.Exception: Error
at Microsoft.FSharp.Control.AsyncBuilderImpl.commit[a](Result`1 res)
at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
at <StartupCode$FSI_0012>.$FSI_0012.main@()

And running this:

  projectToSql "database.com" (fun text cs v -> 
    let textFormat = Printf.TextWriterFormat <string -> unit>(text)
    printfn textFormat cs v) "1.0" "SkuGroupSourcesAddad" ["12ikh412k4"; "1k2jh412kj4h12"] 
  |> Async.RunSynchronously;; 

gave you:

System.Exception: Error
at Microsoft.FSharp.Control.AsyncBuilderImpl.commit[a](Result`1 res)
at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
at <StartupCode$FSI_0013>.$FSI_0013.main@()

So, from these two stack traces it appears that these errors came from the same place, perhaps the dark side of the moon (to infinity and beyond!). The information contained in these stack traces is completely useless in discerning where the errors actually originated from (hint, not the same place).

Now running this code in F# 4.0 we get a subtle but significantly more meaningful result, though.

Running:

  projectToSql "database.com" (fun text cs v -> 
    let textFormat = Printf.TextWriterFormat <string -> unit>(text)
    printfn textFormat cs v) "1.0" "SkuGroupSourcesAdded" ["12ikh412k4"; "1k2jh412kj4h12"] 
  |> Async.RunSynchronously;;

now gives you:

System.Exception: Error
at FSI_0014.updateSkuMappings@72-9.Invoke(Unit unitVar)
at Microsoft.FSharp.Control.AsyncBuilderImpl.callA@851.Invoke(AsyncParams`1 args)
— End of stack trace from previous location where exception was thrown —
at Microsoft.FSharp.Control.AsyncBuilderImpl.commit[a](Result`1 res)
at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
at <StartupCode$FSI_0015>.$FSI_0015.main@()

And running:

  projectToSql "database.com" (fun text cs v -> 
    let textFormat = Printf.TextWriterFormat <string -> unit>(text)
    printfn textFormat cs v) "1.0" "SkuGroupSourcesAddad" ["12ikh412k4"; "1k2jh412kj4h12"] 
  |> Async.RunSynchronously;; 

now gives you:

System.Exception: Error
at FSI_0014.projectToSql@79.Invoke(Unit unitVar)
at Microsoft.FSharp.Control.AsyncBuilderImpl.callA@851.Invoke(AsyncParams`1 args)
— End of stack trace from previous location where exception was thrown —
at Microsoft.FSharp.Control.AsyncBuilderImpl.commit[a](Result`1 res)
at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
at <StartupCode$FSI_0016>.$FSI_0016.main@()

So, we can now see that the first function call produced an error in the updateSkuMappings method, and the second function call produced an error in the enclosing projectToSql method. This may seem somewhat trivial in this example, but when you are using dozens of libraries and you get a stack trace like the ones you see with F# 3.0, you are basically stuck putting try with blocks and debug statements everywhere in your code. With the added information in the new stack traces, you are in a completely different (and much better!) place.

Leave a Reply

Your email address will not be published. Required fields are marked *