首页 > 解决方案 > 为什么 `catch` 没有捕捉到这个异常?

问题描述

我有一个仆人应用程序和一个在数据库中创建记录的端点,然后尝试在 S3 位置之间复制文件。如果复制失败,我想回滚事务。我有这个运营​​商

{-# LANGUAGE TemplateHaskell #-}    

import Control.Monad.Catch
import Control.Monad.Except
import Control.Monad.Logger

(<??)
  :: (MonadError e m, MonadCatch m, MonadLogger m)
  => e
  -> m a
  -> m a
(<??) err a = a `catchAll` (\e -> $(logErrorSH) e >> throwError err)
infixr 0 <??

捕获所有异常,记录异常的性质,然后抛出(在我的情况下,因为我的App类型有一个实例MonadError ServantErr) a ServantErr

我的处理程序是这样的:

{-# LANGUAGE ScopedTypeVariables #-}

import           Control.Monad
import           Control.Monad.Catch
import           Control.Monad.IO.Class
import qualified Network.AWS as AWS
import           Servant

import App.Types
import App.Db


copy :: Copy -> App Text
copy (Copy user bucket srcKey tgtKey) = do
  err400 <?? runDb (insertRecord $ User user bucket srcKey tgtKey)

  catch (err500 <?? liftIO $ do
    env <- AWS.newEnv AWS.Discover
    void . AWS.runResourceT . AWS.runAWS env $ copyFiles bucket srcKey tgtKey
    return "OK") (\(e :: ServantErr) -> rollback e user)
  where rollback e u = runDb (deleteRecord u) >> throwError e

为了测试逻辑,我移动了我的 AWS 凭证文件,期望内部 AWS 操作会抛出一个InvalidFileError,然后(<??)将其转换为 a ServantErr,然后catch会捕获它ServantErr并执行回滚功能。相反,插入成功,InvalidFileError被记录,但回滚永远不会发生(即,执行后记录仍然存在于数据库中)。这个deleteRecord函数在其他地方成功使用,所以我可以确定它的定义不是问题。

知道是什么原因造成的吗?

标签: haskell

解决方案


如果您的App类型最终是 a ExceptT,则问题可能是 theMonadErrorMonadCatchfor 的实例ExceptT不匹配:

  • MonadError实例抛出错误e作为ExceptT e
  • MonadCatch实例捕获底层 monad 中的异常,不是ExceptT e错误。

实例定义MonadCatch (ExceptT e m)

-- | Catches exceptions from the base monad.
instance MonadCatch m => MonadCatch (ExceptT e m) where
   catch (ExceptT m) f = ExceptT $ catch m (runExceptT . f)

ServantErr有一个Exception实例,所以它可以同时抛出。


编辑:“异常”类MonadMask提供了onError功能,即使 forExceptT也表现得非常好:它在ExceptT e异常常规异常的情况下运行清理操作:

仅当主操作中引发错误时才运行操作。与 onException 不同,它适用于各种错误,而不仅仅是异常。例如,如果 f 是一个用 Left 中止的异常计算,计算 onError fg 将执行 g,而 onException fg 不会。

这是比catch处理回滚更好的选择。


推荐阅读