packages feed

grapesy-1.2.0: src/Network/GRPC/Util/Exception/Exact.hs

{-# LANGUAGE CPP #-}

module Network.GRPC.Util.Exception.Exact (
    ExactException(..)
    -- * Catching
  , catchExact
  , tryExact
  , waitCatchExact
    -- * Utilities
  , throwExact
  , withoutAnnotations
  , catchAndWrap
  ) where

import Control.Concurrent.Async
import Control.Concurrent.STM (STM)
import Control.Exception (Exception(..))
import Control.Exception qualified as Base
import Data.Bifunctor
import GHC.Stack

import Network.GRPC.Util.Exception.Shims

{-------------------------------------------------------------------------------
  Definition
-------------------------------------------------------------------------------}

-- | Exception with emphasis on accurate annotations
--
-- When this type appears in the @grapesy@ API, it emphasises that we have tried
-- to ensure that any exception annotations are taken seriously.
--
-- Unlike the 'Exception' instance for 'SomeException', the instance for
-- 'ExactException' can be used with 'throwIO', 'throwTo', 'cancelWith', etc.,
-- without losing any annotations.
--
-- See also 'catchExact', 'tryExact', 'waitCatchExact'.
newtype ExactException = WrapExactException {
      unwrapExactException :: Base.SomeException
    }
  deriving stock (Show)

instance Exception ExactException where
  fromException    = Just . WrapExactException
  toException      = unwrapExactException
  displayException = displayException . unwrapExactException
#if MIN_VERSION_base(4,20,0)
  backtraceDesired = const False
#endif

{-------------------------------------------------------------------------------
  Catching 'ExactException'

  This is primarily useful to avoid accidentally throwing 'SomeException'.
-------------------------------------------------------------------------------}

-- | Catch 'ExactException'
--
-- Won't install any other kind of exception handler (i.e., no 'WhileHandling'
-- annotation will be added). This is comparable to 'catchNoPropagate' (see
-- discussion in 'ExactException'); there is no analogue of 'rethrowIO',
-- since 'throwIO' /itself/ can be used safely with 'ExactException'.
catchExact :: IO a -> (ExactException -> IO a) -> IO a
#if !MIN_VERSION_base(4,21,0)
catchExact = Base.catch
#else
catchExact action handler =
    Base.catchNoPropagate action (handler . aux)
  where
    -- NOTE: only used in GHC 9.12 and up
    aux :: Base.ExceptionWithContext Base.SomeException -> ExactException
    aux (Base.ExceptionWithContext _ctxt se) = WrapExactException se
#endif

tryExact :: IO a -> IO (Either ExactException a)
tryExact = Base.try

waitCatchExact :: Async a -> STM (Either ExactException a)
waitCatchExact = fmap (first WrapExactException) . waitCatchSTM

{-------------------------------------------------------------------------------
  Utilities
-------------------------------------------------------------------------------}

-- | Type-specialized wrapper around throwIO, to avoid mistakes
--
-- This does not need a `HasCallSTack` constraint, because no backtrace is
-- added to `ExactException`.
throwExact :: ExactException -> IO a
throwExact = Base.throwIO

withoutAnnotations :: ExactException -> (forall e. Exception e => e -> r) -> r
withoutAnnotations (WrapExactException (Base.SomeException e)) k = k e

-- | Wrap an exception
--
-- Notes:
--
-- * Since the original exception is wrapped as-is, including any annotations,
--   we use 'catchExact' to avoid adding a 'WhileHandling' annotation.
-- * We use 'throwIO' to throw the new wrapped exception, so that /if/ the
--   exception wrapper has 'backtraceDesired', we get a backtrace to the wrap.
catchAndWrap ::
     (HasCallStack, Exception e)
  => (ExactException -> e) -> IO a -> IO a
catchAndWrap f io = io `catchExact` (throwIO . f)