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 =
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 =
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;)