如何在 F# 的异步工作流程中使用 reraise?

问题描述 投票:0回答:4

我需要在记录异常后重新引发执行异步块时发生的异常。

当我执行以下操作时,编译器认为我没有从处理程序内调用重新引发函数。我做错了什么?

let executeAsync context = async {
    traceContext.Properties.Add("CorrelationId", context.CorrelationId)
    try
        do! runAsync context
        return None
    with
        | e when isCriticalException(e) ->
            logCriticalException e
            reraise()
        | e ->
            logException e
            return Some(e)
}
asynchronous exception f# f#-async
4个回答
19
投票

粗糙!我认为这是不可能的,因为 reraise 对应于一个特殊的 IL 指令,该指令从堆栈顶部捕获异常,但是异步表达式被编译成延续链的方式,我认为语义不成立!

出于同样的原因,以下内容也不会编译:

try
    (null:string).ToString()
with e ->
    (fun () -> reraise())()

在这些情况下,我需要在实际

with
主体之外处理异常,并且想要模拟
reraise
(即保留异常的堆栈跟踪),我使用 this 解决方案,所以总之你的代码看起来像:

let inline reraisePreserveStackTrace (e:Exception) =
    let remoteStackTraceString = typeof<exn>.GetField("_remoteStackTraceString", BindingFlags.Instance ||| BindingFlags.NonPublic);
    remoteStackTraceString.SetValue(e, e.StackTrace + Environment.NewLine);
    raise e

let executeAsync context = async {
    traceContext.Properties.Add("CorrelationId", context.CorrelationId)
    try
        do! runAsync context
        return None
    with
        | e when isCriticalException(e) ->
            logCriticalException e
            reraisePreserveStackTrace e
        | e ->
            logException e
            return Some(e)
}

更新: .NET 4.5 引入了 ExceptionDispatchInfo,这可能允许更清晰地实现上面的

reraisePreserveStackTrace


5
投票

我在不同的环境中遇到了类似的问题,但归根结底就是这样。

异常不能被抛出到不同的线程上 - 调用

reraise()
需要一个异常处理程序在某种意义上运行在代码中原始异步块的“上方”。

let runAsync context = async {return ()}
let isCriticalException e = true
let logCriticalException e = ()
let logException e = ()
let executeAsync context = 
    async {
            do! runAsync context
            return None
}

let run = 
    match executeAsync 5 |> Async.Catch |> Async.RunSynchronously with
    |Choice1Of2(t) -> 
        printfn "%A" t
        None
    |Choice2Of2(exn) ->  
            match exn with
            | e when isCriticalException(e) ->
                logCriticalException e
                raise (new System.Exception("See inner exception",e)) //stack trace will be lost at this point if the exn is not wrapped
            | e ->
                logException e
                Some(e)

注意,我们仍然无法使用重新引发,因为我们现在正在不同的线程上调用,因此我们将异常包装在另一个线程中


4
投票

正如评论和答案更新中提到的,您可以使用

ExceptionDispatchInfo
作为解决方法。

open System.Runtime.ExceptionServices

let inline reraiseAnywhere<'a> (e: exn) : 'a =
    ExceptionDispatchInfo.Capture(e).Throw()
    Unchecked.defaultof<'a>

用途:

async {
    try
         do! someAsyncThing ()
         return "Success"
    with
    | e ->
        if errorIsOk e then return "Handled error"
        else return (reraiseAnyWhere e)
}

这是一个 GitHub Gist 我用单元测试来检查其行为。


0
投票

类似于

@JamesFaix'
的答案...

参见https://github.com/fsharp/fslang-suggestions/issues/660


将其放入

Infrastructure.fs
文件中:

namespace global

type Exception with
    // https://github.com/fsharp/fslang-suggestions/issues/660
    member this.Reraise() =
        System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(this).Throw()
        Unchecked.defaultof<_>

使用示例:

let! client = async {
     try return! CosmosStoreClient.Connect(x, databaseId, containers)
     with e -> Log.Error(e, "Could not connect to {databaseId}/{containers}", databaseId, containers)
               return e.Reraise() }
© www.soinside.com 2019 - 2024. All rights reserved.