在 catch 上下文之外重新抛出异常时如何保留堆栈跟踪?
How to keep the stacktrace when rethrowing an exception out of catch-context?
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。显然,下面的 kind-a 有效,但由于嵌套的异常而使调试变得更加困难,并且如果在深层堆栈中多次调用此 wrap-reraise-wrap-reraise 模式,可能会变得非常难看。
System.Exception("Oops", ex)
|> raise
更新:TeaDrivenDev 在评论中建议使用 ExceptionDispatchInfo.Capture(ex).Throw()
,这可行,但需要将异常包装在其他东西中,从而使模型复杂化。但是,它确实保留了堆栈跟踪,并且可以成为一个相当可行的解决方案。
我担心的一件事是,一旦您将异常视为普通对象并将其传递,您将无法再次引发它并保留其原始堆栈跟踪。
但只有在中间或最后 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
),这将创建以下跟踪(注意带有 的行”--- 从先前发生异常的位置开始的堆栈跟踪结束被抛出 ---"):
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()
(或 C# 中不带参数的 throw
)是一个很好的编程模式,您可以使用它。 reraise()
和 Ex.throwPreserve
之间的唯一区别是后者不需要 catch
上下文,我认为这是一个巨大的可用性增益。
我想这最终是一个品味问题和你习惯的问题。对我来说,我只想将异常的原因放在首位。非常感谢第一位评论者 TeaDrivenDev,他指导我进行 .NET 4.5 增强,这本身导致了上述第二种方法。
(很抱歉回答了我自己的问题,但由于 none 的评论者这样做了,我决定加强 ;)
对于那些错过了 "out of catch-context" 要点的人(比如我)- 您可以使用 reraise() 来在从 catch 块中抛出时保留堆栈。
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。显然,下面的 kind-a 有效,但由于嵌套的异常而使调试变得更加困难,并且如果在深层堆栈中多次调用此 wrap-reraise-wrap-reraise 模式,可能会变得非常难看。
System.Exception("Oops", ex)
|> raise
更新:TeaDrivenDev 在评论中建议使用 ExceptionDispatchInfo.Capture(ex).Throw()
,这可行,但需要将异常包装在其他东西中,从而使模型复杂化。但是,它确实保留了堆栈跟踪,并且可以成为一个相当可行的解决方案。
我担心的一件事是,一旦您将异常视为普通对象并将其传递,您将无法再次引发它并保留其原始堆栈跟踪。
但只有在中间或最后 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
),这将创建以下跟踪(注意带有 的行”--- 从先前发生异常的位置开始的堆栈跟踪结束被抛出 ---"):
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()
(或 C# 中不带参数的 throw
)是一个很好的编程模式,您可以使用它。 reraise()
和 Ex.throwPreserve
之间的唯一区别是后者不需要 catch
上下文,我认为这是一个巨大的可用性增益。
我想这最终是一个品味问题和你习惯的问题。对我来说,我只想将异常的原因放在首位。非常感谢第一位评论者 TeaDrivenDev,他指导我进行 .NET 4.5 增强,这本身导致了上述第二种方法。
(很抱歉回答了我自己的问题,但由于 none 的评论者这样做了,我决定加强 ;)
对于那些错过了 "out of catch-context" 要点的人(比如我)- 您可以使用 reraise() 来在从 catch 块中抛出时保留堆栈。