TL;DR: how to raise a previously caught exception later on, while preserving the original exception's stacktrace.
Since I think this is useful with the Result
monad or computation expression, esp. since that pattern is often used for wrapping an exception without throwing it, here's a worked out example of that:
type Result<'TResult, 'TError> =
| Success of 'TResult
| Fail of 'TError
module Result =
let bind f =
function
| Success v -> f v
| Fail e -> Fail e
let create v = Success v
let retnFrom v = v
type ResultBuilder () =
member __.Bind (m , f) = bind f m
member __.Return (v) = create v
member __.ReturnFrom (v) = retnFrom v
member __.Delay (f) = f
member __.Run (f) = f()
member __.TryWith (body, handler) =
try __.Run body
with e -> handler e
[<AutoOpen>]
module ResultBuilder =
let result = Result.ResultBuilder()
And now let's use it:
module Extern =
let calc x y = x / y
module TestRes =
let testme() =
result {
let (x, y) = 10, 0
try
return Extern.calc x y
with e ->
return! Fail e
}
|> function
| Success v -> v
| Fail ex -> raise ex // want to preserve original exn's stacktrace here
The problem is that the stacktrace will not include the source of the exception (here namely the calc
function). If I run the code as written, it will throw as follows, which gives no information to the origin of the error:
System.DivideByZeroException : Attempted to divide by zero.
at Microsoft.FSharp.Core.Operators.Raise[T](Exception exn)
at PlayFul.TestRes.testme() in D:\Experiments\Play.fs:line 197
at PlayFul.Tests.TryItOut() in D:\Experiments\Play.fs:line 203
Using reraise()
won't work, it wants a catch-context. Obviously, the following kind-a works, but makes debugging harder because of the nested exceptions and could get pretty ugly if this wrap-reraise-wrap-reraise pattern gets called multiple times in a deep stack.
System.Exception("Oops", ex)
|> raise
Update: TeaDrivenDev suggested in the comments to use ExceptionDispatchInfo.Capture(ex).Throw()
, which works, but requires to wrap the exception in something else, complicating the model. However, it does preserve the stacktrace and it can be made into a fairly workable solution.
One of the things I was afraid of is that once you treat an exception as a normal object and pass it around, you won't be able to raise it again and keep its original stacktrace.
But that's only true if you do, in-between or at the end, a
raise excn
.I have taken all the ideas from the comments and show them here as three solutions to the problem. Choose whichever feels most natural to you.
Capture the stack trace with ExceptionDispatchInfo
The following example shows TeaDrivenDev's proposal in action, using
ExceptionDispatchInfo.Capture
.With the example in the original question (replace
raise ex
), this will create the following trace (note the line with "--- End of stack trace from previous location where exception was thrown ---"):Preserve the stacktrace completely
If you don't have .NET 4.5, or don't like the added line in the middle of the trace ("--- End of stack trace from previous location where exception was thrown ---"), then you can preserve the stack and add the current trace in one go.
I found this solution by following TeaDrivenDev's solution and happened upon Preserving stacktrace when rethrowing exceptions.
With the example in the original question (replace
raise ex
), you will see that the stacktraces are nicely coupled and that the origin of the exception is on the top, where it should be:Wrap the exception in an exception
This was suggested by Fyodor Soikin, and is probably the .NET default way, as it is used in many cases in the BCL. However, it results in a less-then-useful stacktrace in many situations and, imo, can lead to confusing topsy-turvy traces in deeply nested functions.
Applied in the same way (replace
raise ex
) as the previous examples, this will give you a stacktrace as follows. In particular, note that the root of the exception, thecalc
function, is now somewhere in the middle (still quite obvious here, but in deep traces with multiple nested exceptions, not so much anymore).Also note that this is a trace dump that honors the nested exception. When you are debugging, you need to click through all nested exceptions (and realize is it nested to begin with).
Conclusion
I'm not saying one approach is better than another. To me, just mindlessly doing
raise ex
is not a good idea, unlessex
is a newly created and not previously raised exception.The beauty is that
reraise()
effectively does the same as the theEx.throwPreserve
does above. So if you thinkreraise()
(orthrow
without arguments in C#) is a good programming pattern, you can use that. The only difference betweenreraise()
andEx.throwPreserve
is that the latter does not require acatch
context, which I believe is a huge usability gain.I guess in the end this is a matter of taste and what you're used to. To me, I just want the cause of the exception prominently on top. Major thanks for the first commenter, TeaDrivenDev who directed me to the .NET 4.5 enhancement, which itself led to the 2nd approach above.
(apologies for answering my own question, but since none of the commenters did it, I decided to step up ;)