How to save stacktrace when rebuilding an exception from a catch context?

TL; DR: how to raise a previously caught exception later while maintaining the original stacktrace exception.

Since I think this is useful for expressing Result monad or computation, esp. since this pattern is often used to wrap an exception without throwing it, here is a worked-out example of this:

 type Result<'TResult, 'TError> = | Success of 'TResult | Fail of 'TError module Result = let bind f = function | Success v -> fv | Fail e -> Fail e let create v = Success v let retnFrom v = v type ResultBuilder () = member __.Bind (m , f) = bind fm 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() 

Now let's use it:

 module Extern = let calc xy = x / y module TestRes = let testme() = result { let (x, y) = 10, 0 try return Extern.calc xy with e -> return! Fail e } |> function | Success v -> v | Fail ex -> raise ex // want to preserve original exn stacktrace here 

The problem is that stacktrace will not include the exception source (here the calc function). If I run the code as written, it will throw the following, which does not give any information about 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() will not work; it requires a catch context. Obviously, the following view is work, but it makes debugging more complicated due to nested exceptions and can become quite ugly if this wrapper-reraise-wrapper-reraise template is called several times in a deep stack.

 System.Exception("Oops", ex) |> raise 

Update: TeaDrivenDev suggested using ExceptionDispatchInfo.Capture(ex).Throw() in the comments, which works, but needs to wrap the exception in something else, which complicates the model. However, it saves stacktrace and can be turned into a pretty workable solution.

+7
try-catch monads f # computation-expression
source share
1 answer

One of the things that I was afraid of is that once you handle the exception as a regular object and pass it on, you cannot pick it up again and maintain your original glassy structure.

But this is only true if you do, between or at the end, raise excn .

I took all the ideas from the comments and show them here as three solutions to the problem. Choose what seems most natural to you.

Grip stacktrace

The following example shows the TeaDrivenDev clause in action using ExceptionDispatchInfo.Capture .

 type Ex = /// Capture exception (.NET 4.5+), keep the stack, add current stack. /// This puts the origin point of the exception on top of the stacktrace. /// It also adds a line in the trace: /// "--- End of stack trace from previous location where exception was thrown ---" static member inline throwCapture ex = ExceptionDispatchInfo.Capture ex |> fun disp -> disp.Throw() failwith "Unreachable code reached." 

In the example in the original question (replace raise ex ), this will create the following trace (note the line with "--- The end of the stack trace from the previous location where the exception was thrown ---"):

 System.DivideByZeroException : Attempted to divide by zero. at Playful.Ex.Extern.calc(Int32 x, Int32 y) in R:\path\Ex.fs:line 118 at Playful.Ex.TestRes.testme@137-1.Invoke (Unit unitVar) in R:\path\Ex.fs:line 137 at Playful.Ex.Result.ResultBuilder.Run[b](FSharpFunc`2 f) in R:\path\Ex.fs:line 103 at Playful.Ex.Result.ResultBuilder.TryWith[a](FSharpFunc`2 body, FSharpFunc`2 handler) in R:\path\Ex.fs:line 105 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at Playful.Ex.TestRes.testme() in R:\path\Ex.fs:line 146 at Playful.Ex.Tests.TryItOut() in R:\path\Ex.fs:line 153 

Save the stack completely

If you don’t have .NET 4.5 or don’t like the added line in the middle of the trace ("--- End stack trace from the previous location where the exception was thrown ---"), then you can save the stack and add the current trace at a time .

I found this solution by executing TeaDrivenDev solution and happened after saving stacktrace on repeated exception .

 type Ex = /// Modify the exception, preserve the stacktrace and add the current stack, then throw (.NET 2.0+). /// This puts the origin point of the exception on top of the stacktrace. static member inline throwPreserve ex = let preserveStackTrace = typeof<Exception>.GetMethod("InternalPreserveStackTrace", BindingFlags.Instance ||| BindingFlags.NonPublic) (ex, null) |> preserveStackTrace.Invoke // alters the exn, preserves its stacktrace |> ignore raise ex 

In the example in the original question (replace raise ex ) you will see that stacktraces are well connected and that the start of the exception is at the top where it should be:

 System.DivideByZeroException : Attempted to divide by zero. at Playful.Ex.Extern.calc(Int32 x, Int32 y) in R:\path\Ex.fs:line 118 at Playful.Ex.TestRes.testme@137-1.Invoke (Unit unitVar) in R:\path\Ex.fs:line 137 at Playful.Ex.Result.ResultBuilder.Run[b](FSharpFunc`2 f) in R:\path\Ex.fs:line 103 at Playful.Ex.Result.ResultBuilder.TryWith[a](FSharpFunc`2 body, FSharpFunc`2 handler) in R:\path\Ex.fs:line 105 at Microsoft.FSharp.Core.Operators.Raise[T](Exception exn) at Playful.Ex.TestRes.testme() in R:\path\Ex.fs:line 146 at Playful.Ex.Tests.TryItOut() in R:\path\Ex.fs:line 153 

Wrap an exception into an exception

This was suggested by Fedor Soikin , and it is probably the default .NET method, as it is used in many cases in BCL. However, this leads to a less useful stacktrace in many situations and, imo, can lead to confusing interlaced tracks in deeply nested functions.

 type Ex = /// Wrap the exception, this will put the Core.Raise on top of the stacktrace. /// This puts the origin of the exception somewhere in the middle when printed, or nested in the exception hierarchy. static member inline throwWrapped ex = exn("Oops", ex) |> raise 

It is applied in the same way (replace raise ex ) as the previous examples, this will give you stacktrace as follows. In particular, note that the root of the exception, the calc function, is now somewhere in the middle (still quite obvious here, but not many in deep traces with a few nested exceptions).

Also note that this is a trace dump that distinguishes a nested exception. When you are debugging, you need to click all the nested exceptions (and understand why it is nested).

 System.Exception : Oops ----> System.DivideByZeroException : Attempted to divide by zero. at Microsoft.FSharp.Core.Operators.Raise[T](Exception exn) at Playful.Ex.TestRes.testme() in R:\path\Ex.fs:line 146 at Playful.Ex.Tests.TryItOut() in R:\path\Ex.fs:line 153 --DivideByZeroException at Playful.Ex.Extern.calc(Int32 x, Int32 y) in R:\path\Ex.fs:line 118 at Playful.Ex.TestRes.testme@137-1.Invoke (Unit unitVar) in R:\path\Ex.fs:line 137 at Playful.Ex.Result.ResultBuilder.Run[b](FSharpFunc`2 f) in R:\path\Ex.fs:line 103 at Playful.Ex.Result.ResultBuilder.TryWith[a](FSharpFunc`2 body, FSharpFunc`2 handler) in R:\path\Ex.fs:line 105 

Conclusion

I am not saying that one approach is better than another. For me, just the thoughtless execution of raise ex not a good idea, if only ex is a newly created and previously excluded exception.

The beauty is that reraise() does the exact same thing as the previous Ex.throwPreserve . Therefore, if you think reraise() (or throw without arguments in C #) is a good programming pattern, you can use it. The only difference between reraise() and Ex.throwPreserve is that the latter does not require a catch context, which in my opinion is a huge usability factor.

I think, in the end, it is a matter of taste and what you are used to. For me, I just want the reason for exclusion to take a prominent place. Thanks a lot for the first commenter, TeaDrivenDev , who directed me to improve .NET 4.5, which in itself led to the second approach above.

(apologies for the answer to my own question, but since none of the commentators did this, I decided to activate;)

+6
source share

All Articles