packages feed

atelier-core-0.2.0.0: src/Atelier/Exception.hs

{-# OPTIONS_GHC -Wno-redundant-constraints #-}

-- | Helpers for telling synchronous exceptions apart from asynchronous ones.
--
-- Synchronous exceptions are genuine failures worth catching, logging, or
-- retrying; asynchronous ones (thread cancellation, @SIGINT@) usually signal an
-- intentional shutdown and should be allowed to propagate. The @*SyncIO@
-- helpers catch only the former and re-throw the latter.
module Atelier.Exception
    ( isGracefulShutdown
    , trySyncIO
    , catchSyncIO
    , isSyncException
    ) where

import Effectful.Exception (SomeAsyncException, isSyncException)

import Control.Exception qualified as E


-- | Determine if an exception represents a graceful shutdown (any async exception).
--
-- Async exceptions indicate intentional cancellation (e.g., Ki's ScopeClosing,
-- or UserInterrupt from SIGINT), as opposed to real errors that warrant logging
-- or retry logic.
isGracefulShutdown :: SomeException -> Bool
isGracefulShutdown = isJust . fromException @SomeAsyncException


-- | Like `Control.Exception.try`, but will only catch synchronous exceptions.
trySyncIO :: IO a -> IO (Either SomeException a)
trySyncIO f = withFrozenCallStack catchSyncIO (fmap Right f) (pure . Left)


-- | Like @Control.Exception.catch@, but only synchronous exceptions reach the
-- handler; asynchronous exceptions are re-thrown unchanged.
catchSyncIO :: (HasCallStack) => IO a -> (SomeException -> IO a) -> IO a
catchSyncIO f g =
    f `E.catch` \e ->
        if isSyncException e then
            g e
        else
            E.throwIO e