在 F# 异步块中捕获内部异常

Catching inner exception in F# async block

我有一个异步块,在这个块中我从外部 C# Web 服务客户端库调用一个异步方法。此方法调用 returns 数据传输对象或类型 ApiException 的自定义异常。最初,我的函数看起来像这样:

    type Msg =
        | LoginSuccess
        | LoginError
        | ApiError

    let authUserAsync (client: Client) model =
        async {
            do! Async.SwitchToThreadPool()
            let loginParams = new LoginParamsDto(Username = "some username", Password = "some password")
            try
                // AuthenticateAsync may throw a custom ApiException.
                let! loggedInAuthor = client.AuthenticateAsync loginParams |> Async.AwaitTask                
                // Do stuff with loggedInAuthor DTO...
                return LoginSuccess
            with
            | :? ApiException as ex ->
                let msg = 
                    match ex.StatusCode with
                    | 404 -> LoginError
                    | _ -> ApiError
                return msg
        }

但我发现 ApiException 没有被捕获。进一步调查显示 ApiException 实际上是内部异常。所以我将代码更改为:

    type Msg =
        | LoginSuccess
        | LoginError
        | ApiError

    let authUserAsync (client: Client) model =
        async {
            do! Async.SwitchToThreadPool()
            let loginParams = new LoginParamsDto(Username = "some username", Password = "some password")
            try
                // AuthenticateAsync may throw a custom ApiException.
                let! loggedInAuthor = client.AuthenticateAsync loginParams |> Async.AwaitTask                
                // Do stuff with loggedInAuthor DTO...
                return LoginSuccess
            with
            | baseExn ->
                let msg = 
                    match baseExn.InnerException with
                    | :? ApiException as e -> 
                        match e.StatusCode with
                        | 404 -> LoginError
                        | _ -> ApiError
                    | otherExn -> raise otherExn
                return msg
        }

这似乎行得通。但是作为 F# 的新手,我想知道在这种情况下是否有更优雅或更惯用的方法来捕获内部异常?

我有时使用 active pattern 来捕获主要或内部异常:

let rec (|NestedApiException|_|) (e: exn) =
    match e with
    | null -> None
    | :? ApiException as e -> Some e
    | e -> (|NestedApiException|_|) e.InnerException

然后像这样使用它:

async {
    try
        ...
    with
    | NestedApiException e ->
        ...
    | otherExn ->
        raise otherExn
}

基于来自 Tarmil 的活动模式的解决方案非常好,如果我想在文件或项目的多个位置捕获异常,我会使用它。但是,如果我只想在一个地方执行此操作,那么我可能不想为它定义一个单独的活动模式。

使用 try ... with 表达式中的 when 子句可以更好地编写您所拥有的内容:

let authUserAsync (client: Client) model =
  async {
    try
      // (...)
    with baseExn when (baseExn.InnerException :? ApiException) ->
      let e = baseExn :?> ApiException
      match e.StatusCode with
      | 404 -> return LoginError
      | _ -> return ApiError }

这有点重复,因为您必须使用 :? 进行类型检查,然后再次使用 :?> 进行类型转换,但这是一种更好的内联方式。

作为替代方案,如果您使用 F# 异常,则可以使用适用于产品类型的全部模式匹配。错误处理代码读起来更简洁。

exception TimeoutException
exception ApiException of message: string * inner: exn

try
    action ()
with
| ApiException (_, TimeoutException) ->
    printfn "Timed out"
| ApiException (_, (:? ApplicationException as e)) ->
    printfn "%A" e