如何在catch-context中重新抛出异常时保持堆栈跟踪?

时间:2016-12-16 23:01:06

标签: f# try-catch monads computation-expression

TL; DR:稍后如何引发先前捕获的异常,同时保留原始异常的堆栈跟踪。

因为我认为这对Result monad或计算表达式很有用,尤其是因为该模式通常用于包装异常而不抛出异常,所以这里有一个很好的例子:

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()

现在让我们使用它:

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

问题是堆栈跟踪不包含异常的来源(这里是calc函数)。如果我按照编写的方式运行代码,它将抛出如下内容,它不会提供错误来源的信息:

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

使用reraise()无效,它需要一个catch-context。显然,以下类型 - 一个工作,但由于嵌套异常使调试更难,如果这个wrap-reraise-wrap-reraise模式在深层栈中被多次调用,可能会变得非常难看。

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

更新:TeaDrivenDev在评论中建议使用ExceptionDispatchInfo.Capture(ex).Throw(),这有效,但需要将异常包装在其他内容中,使模型复杂化。但是,它确实保留了堆栈跟踪,并且它可以成为一个相当可行的解决方案。

2 个答案:

答案 0 :(得分:6)

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

但是,只有当你在raise excn之间或之后这样做时才会这样。

我已经从评论中获取了所有想法,并在此将其显示为问题的三种解决方案。选择最适合自己的方式。

捕获堆栈跟踪

以下示例使用ExceptionDispatchInfo.Capture显示了TeaDrivenDev的提案。

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),这将创建以下跟踪(注意带有的行“---从抛出异常的先前位置开始的堆栈跟踪结束 - - “):

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,或者不喜欢跟踪中间添加的行(“---从先前位置抛出异常的堆栈跟踪结束---” ),然后您可以保留堆栈一次添加当前跟踪。

我通过遵循TeaDrivenDev的解决方案找到了这个解决方案,并发生在Preserving stacktrace when rethrowing exceptions上。

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增强,这本身就是第二种方法。

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

答案 1 :(得分:2)

对于那些错过了“out-context”(像我一样)这一点的人 - 你可以使用reraise()来保护堆栈,从catch块中抛出。