Как сохранить stacktrace при переосмыслении исключения из контекста catch?

TL; DR: как поднять ранее пойманное исключение позже, сохраняя при этом stacktrace исходного исключения.

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

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 будет не включать источник за исключением (здесь именно

2 ответов


но это верно только в том случае, если вы делаете, между или в конце,raise excn.

я взял все идеи из комментариев и показать их здесь три пути решения проблемы. Выберите то, что кажется вам наиболее естественным.

захват трассировка стека

в следующем примере показано предложение 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

сохранить stacktrace полностью

если у вас нет .NET 4.5 или вам не нравится добавленная строка в середина следа ( " - - - конец трассировки стека из предыдущего местоположения, где было создано исключение - - -"), то вы можете сохранить стек и добавьте текущую трассировку за один раз.

я нашел это решение, следуя решению TeaDrivenDev и случилось на сохранение 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), вы увидите, что stacktraces хорошо связаны и что происхождение исключения находится сверху, где оно должно быть:

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

оберните исключение в исключение

это было предложено Петрович Сойкин Федор и, вероятно, является способом .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) как и в предыдущих примерах, это даст вам 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 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 enhancement, что само по себе привело к 2-му подходу выше.

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


для тех, кто пропустил пункт о "out of catch-context" (например, я) - вы можете использовать reraise () для сохранения стека при метании из блока catch.