16

I'd like to test the following async workflow (with NUnit+FsUnit):

let foo = async {
  failwith "oops"
  return 42
}

I wrote the following test for it:

let [<Test>] TestFoo () =
  foo
  |> Async.RunSynchronously
  |> should equal 42

Since foo throws I get the following stacktrace in the unit test runner:

System.Exception : oops
   at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously(CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
   at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously(FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
   at ExplorationTests.TestFoo() in ExplorationTests.fs: line 76

Unfortunately the stacktrace doesn't tell me where the exception was raised. It stops at RunSynchronously.

Somewhere I heard that Async.Catch magically restores the stacktrace, so I adjusted my test:

let [<Test>] TestFooWithBetterStacktrace () =
  foo
  |> Async.Catch
  |> Async.RunSynchronously
  |> fun x -> match x with 
              | Choice1Of2 x -> x |> should equal 42
              | Choice2Of2 ex -> raise (new System.Exception(null, ex))

Now this is ugly but at least it produces a useful stacktrace:

System.Exception : Exception of type 'System.Exception' was thrown.
  ----> System.Exception : oops
   at Microsoft.FSharp.Core.Operators.Raise(Exception exn)
   at ExplorationTests.TestFooWithBetterStacktrace() in ExplorationTests.fs: line 86
--Exception
   at Microsoft.FSharp.Core.Operators.FailWith(String message)
   at [email protected](Unit unitVar) in ExplorationTests.fs: line 71
   at [email protected](AsyncParams`1 args)

This time the stacktrace shows exactly where the error happend: ExplorationTests.foo@line 71

Is there a way to get rid of the Async.Catch and the matching between two choices while still getting useful stacktraces? Is there a better way to structure async workflow tests?

3
  • 1
    I had the same problem and Async.Catch was the only workaround I could find Commented Aug 12, 2013 at 16:51
  • I have emailed Don Syme, who suggested it was a fundamental .NET limitation, and that Async.Catch was the only option. Commented Aug 12, 2013 at 21:40
  • @JohnPalmer sounds like an answer to me Commented Aug 12, 2013 at 21:45

2 Answers 2

7

Since Async.Catch and rethrowing the exception seem to be the only way to get a useful stacktrace I came up with the following:

type Async with
  static member Rethrow x =
    match x with 
      | Choice1Of2 x -> x
      | Choice2Of2 ex -> ExceptionDispatchInfo.Capture(ex).Throw()
                         failwith "nothing to return, but will never get here"

Note "ExceptionDispatchInfo.Capture(ex).Throw()". That's about the nicest way one can rethrow an exception without corrupting its stacktrace (downside: only available since .NET 4.5).

Now I can rewrite the test "TestFooWithBetterStacktrace" like that:

let [<Test>] TestFooWithBetterStacktrace () =
  foo
  |> Async.Catch
  |> Async.RunSynchronously
  |> Async.Rethrow
  |> should equal 42

The test looks much better, the rethrowing code doesn't suck (as much as before) and I get useful stacktraces in the test runner when something goes wrong.

Sign up to request clarification or add additional context in comments.

Comments

4

Quoting from some emails that I sent Don Syme a while back:

The debug experience should improve if you try setting "Catch First Chance Exceptions" in Debug --> Exceptions --> CLR Exceptions. Turning off "Just My Code" can also help.

and

Right. With async { ... }, computations are not be stack bound, hence exceptions need to be rethrown in some places to get them back to the right thread.

Judicious use of Async.Catch, or other exception handling can also help.

1 Comment

hi john, quote #1 enabling breaking on first chance exceptions doesnt really help much since i hardly ever use the debugger. i need useful stacktraces in the output of the unit test runner. quote #2 does that mean that i'm already doing it right in my 2nd test (TestFooWithBetterStacktrace)?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.