11

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.

4

2 回答 2

10

我害怕的一件事是,一旦您将异常视为普通对象并传递它,您将无法再次引发它并保留其原始堆栈跟踪。

但这只有在你这样做时才是真的,在中间或最后,a raise excn.

我从评论中汲取了所有想法,并在这里将它们作为解决问题的三个解决方案来展示。选择你觉得最自然的那个。

使用 ExceptionDispatchInfo 捕获堆栈跟踪

下面的例子展示了 TeaDrivenDev 的提案在行动中,使用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."

使用原始问题中的示例(替换raise ex),这将创建以下跟踪(注意带有“--- End of stack trace from previous location where exception was throwed ---”的行):

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

完全保留堆栈跟踪

如果您没有 .NET 4.5,或者不喜欢在跟踪中间添加的行(“--- End of stack trace from previous location where exception was throwed ---”),那么您可以保留堆栈一次性添加当前跟踪。

我通过遵循 TeaDrivenDev 的解决方案找到了这个解决方案,并且在重新抛出异常时发生了 Preserving stacktrace

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

使用原始问题(替换raise ex)中的示例,您将看到堆栈跟踪很好地耦合,并且异常的起源在顶部,它应该在哪里:

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

将异常包装在异常中

这是Fyodor Soikin建议的,并且可能是 .NET 默认方式,因为它在 BCL 中的许多情况下使用。但是,在许多情况下,它会导致堆栈跟踪不太有用,并且在 imo 中,可能会导致深度嵌套函数中混乱的跟踪混乱。

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

以与前面示例相同的方式(替换raise ex)应用,这将为您提供如下堆栈跟踪。特别要注意,异常的根源,即calc函数,现在位于中间的某个位置(这里仍然很明显,但在具有多个嵌套异常的深层跟踪中,不再那么明显了)。

另请注意,这是一个遵循嵌套异常的跟踪转储。在调试时,您需要单击所有嵌套异常(并意识到它是否嵌套开始)。

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

结论

我并不是说一种方法比另一种更好。对我来说,只是盲目地做raise ex并不是一个好主意,除非ex是一个新创建的并且之前没有引发的异常。

美妙之处在于它reraise()有效地与上述相同Ex.throwPreserve。因此,如果您认为reraise()(或throw在 C# 中不带参数)是一种很好的编程模式,您可以使用它。reraise()和之间的唯一区别Ex.throwPreserve是后者不需要catch上下文,我认为这是一个巨大的可用性增益。

我想这最终是一个品味问题和你习惯的问题。对我来说,我只想把异常的原因放在首位。非常感谢第一位评论者TeaDrivenDev,他将我引导至 .NET 4.5 增强,这本身导致了上面的第二种方法。

(很抱歉回答我自己的问题,但由于没有评论者这样做,我决定加强;)

于 2016-12-17T19:21:12.013 回答
4

对于那些错过了“脱离捕获上下文”这一点的人(比如我)——你可以使用 reraise() 从捕获块抛出时保留堆栈。

于 2018-04-10T12:26:13.797 回答