Как сохранить StackTrace когда Повторное выбрасывание исключения из броского контекста?

Обновить

November 2018

Просмотры

617 раз

7

TL; DR: как поднять ранее пойманное исключение в дальнейшем, при сохранении StackTrace исходного Exception в.

Так как я думаю , что это полезно с Resultвыражением монады или вычислений, особами. так что модель часто используется для упаковки исключения , не бросать его, вот это отработанный пример того , что:

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

Проблема заключается в том, что StackTrace будет не включать источник исключения (здесь именно 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()не будет работать, он хочет кетчуп контекст. Очевидно, что следующий вид-а работает, но делает отладку сложнее из-за вложенные исключения и может получить довольно уродливым , если это обертка-ререйз-обертывание-ререйз шаблон вызывается несколько раз в глубоком стеке.

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

Обновление: TeaDrivenDev предложил в комментариях использовать ExceptionDispatchInfo.Capture(ex).Throw(), который работает, но требует , чтобы обернуть исключение в чем - то еще, усложняя модель. Тем не менее, это действительно сохранить StackTrace и это может быть сделано в достаточно эффективное решение.

2 ответы

6

One of the things I was afraid of is that once you treat an exception as a normal object and pass it around, you won't be able to raise it again and keep its original stacktrace.

But that's only true if you do, in-between or at the end, a raise excn.

I have taken all the ideas from the comments and show them here as three solutions to the problem. Choose whichever feels most natural to you.

Capture the stacktrace

The following example shows TeaDrivenDev's proposal 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."

With the example in the original question (replace raise ex), this will create the following trace (note the line with "--- End of stack trace from previous location where 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 [email protected](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

Preserve the stacktrace completely

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

I found this solution by following TeaDrivenDev's solution and happened upon 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

With the example in the original question (replace raise ex), you will see that the stacktraces are nicely coupled and that the origin of the exception is on 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 [email protected](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 the exception in an exception

Это было предложено Федора Сойкин , и, возможно , путь .NET по умолчанию, так как он используется во многих случаях в BCL. Тем не менее, это приводит к менее тогда полезный StackTrace во многих ситуациях и, имо, может привести к путанице кувырком следам в глубоко вложенных функциях.

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) , как в предыдущих примерах, это даст вам StackTrace следующим образом . В частности, обратите внимание , что корень исключения, 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 [email protected](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, что сам по себе привело к 2 подходу выше.

(извинения за ответ на мой собственный вопрос, но так как никто из комментаторов не сделал это, я решил подойти;)

1

Для тех, кто пропустил пункт о «из броского контекста» (как я) - вы можете использовать ререйз (), чтобы сохранить стек при метании из улова блока.

Связанные вопросы