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

时间:2021-02-01 09:40:05

标签: asynchronous exception f# c#-to-f#

我有一个异步块,在这个块中,我从外部 C# Web 服务客户端库调用了一个异步方法。此方法调用返回数据传输对象,或类型为 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# 新手,我想知道在这种情况下是否有更优雅或更惯用的方法来捕获内部异常?

3 个答案:

答案 0 :(得分:4)

我有时使用 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
}

答案 1 :(得分:2)

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

when 表达式中使用 try ... with 子句有一种更好的书写方式:

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 }

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

答案 2 :(得分:0)

作为替代方案,如果您使用 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