Skip to content

Commit

Permalink
Improve Show instance of Effectful.Error.Static.ErrorWrapper
Browse files Browse the repository at this point in the history
For example, if a third-party library spawns worker threads and if a job throws,
it simply catches SomeException, logs it and retries the job later, as of today
internal exceptions representing errors of the `Error` effect wouldn't show any
info about the error, just the backtrace.

This fixes it at the cost of small API breakage.
  • Loading branch information
arybczak committed Aug 26, 2024
1 parent 9bf15a2 commit a022b49
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 21 deletions.
12 changes: 10 additions & 2 deletions effectful-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
* Improve `Effectful.Labeled`, add `Effectful.Labeled.Error`,
`Effectful.Labeled.Reader`, `Effectful.Labeled.State` and
`Effectful.Labeled.Writer`.
* **Breaking change**: `localSeqLend`, `localLend`, `localSeqBorrow` and
`localBorrow` now take a list of effects instead of a single one.
* Add `throwErrorWith` and `throwError_` to `Effectful.Error.Static` and
`Effectful.Error.Dynamic`.
* **Breaking changes**:
- `localSeqLend`, `localLend`, `localSeqBorrow` and `localBorrow` now take a
list of effects instead of a single one.
- `Effectful.Error.Static.throwError` now requires the error type to have a
`Show` constraint. If this is not the case for some of your error types, use
`throwError_` for them.
- `ThrowError` operation from the dynamic version of the `Error` effect was
replaced with `ThrowErrorWith`.

# effectful-core-2.3.1.0 (2024-06-07)
* Drop support for GHC 8.8.
Expand Down
34 changes: 30 additions & 4 deletions effectful-core/src/Effectful/Error/Dynamic.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ module Effectful.Error.Dynamic
, runErrorNoCallStackWith

-- ** Operations
, throwErrorWith
, throwError
, throwError_
, catchError
, handleError
, tryError
Expand All @@ -34,7 +36,8 @@ import Effectful.Error.Static qualified as E

-- | Provide the ability to handle errors of type @e@.
data Error e :: Effect where
ThrowError :: e -> Error e m a
-- | @since 2.4.0.0
ThrowErrorWith :: (e -> String) -> e -> Error e m a
CatchError :: m a -> (E.CallStack -> e -> m a) -> Error e m a

type instance DispatchOf (Error e) = Dynamic
Expand All @@ -44,7 +47,7 @@ runError
:: Eff (Error e : es) a
-> Eff es (Either (E.CallStack, e) a)
runError = reinterpret E.runError $ \env -> \case
ThrowError e -> E.throwError e
ThrowErrorWith display e -> E.throwErrorWith display e
CatchError m h -> localSeqUnlift env $ \unlift -> do
E.catchError (unlift m) (\cs -> unlift . h cs)

Expand Down Expand Up @@ -81,13 +84,36 @@ runErrorNoCallStackWith handler m = runErrorNoCallStack m >>= \case
Left e -> handler e
Right a -> pure a

-- | Throw an error of type @e@.
-- | Throw an error of type @e@ and specify a display function in case a
-- third-party code catches the internal exception and 'show's it.
--
-- @since 2.4.0.0
throwErrorWith
:: (HasCallStack, Error e :> es)
=> (e -> String)
-- ^ The display function.
-> e
-- ^ The error.
-> Eff es a
throwErrorWith display = withFrozenCallStack send . ThrowErrorWith display

-- | Throw an error of type @e@ with 'show' as a display function.
throwError
:: (HasCallStack, Error e :> es, Show e)
=> e
-- ^ The error.
-> Eff es a
throwError = withFrozenCallStack throwErrorWith show

-- | Throw an error of type @e@ with no display function.
--
-- @since 2.4.0.0
throwError_
:: (HasCallStack, Error e :> es)
=> e
-- ^ The error.
-> Eff es a
throwError e = withFrozenCallStack $ send (ThrowError e)
throwError_ = withFrozenCallStack throwErrorWith (const "<opaque>")

-- | Handle an error of type @e@.
catchError
Expand Down
49 changes: 39 additions & 10 deletions effectful-core/src/Effectful/Error/Static.hs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ module Effectful.Error.Static
, runErrorNoCallStackWith

-- ** Operations
, throwErrorWith
, throwError
, throwError_
, catchError
, handleError
, tryError
Expand Down Expand Up @@ -162,15 +164,38 @@ runErrorNoCallStackWith handler m = runErrorNoCallStack m >>= \case
Left e -> handler e
Right a -> pure a

-- | Throw an error of type @e@.
-- | Throw an error of type @e@ and specify a display function in case a
-- third-party code catches the internal exception and 'show's it.
--
-- @since 2.4.0.0
throwErrorWith
:: forall e es a. (HasCallStack, Error e :> es)
=> (e -> String)
-- ^ The display function.
-> e
-- ^ The error.
-> Eff es a
throwErrorWith display e = unsafeEff $ \es -> do
Error eid <- getEnv @(Error e) es
throwIO $ ErrorWrapper eid callStack (display e) (toAny e)

-- | Throw an error of type @e@ with 'show' as a display function.
throwError
:: forall e es a. (HasCallStack, Error e :> es, Show e)
=> e
-- ^ The error.
-> Eff es a
throwError = withFrozenCallStack throwErrorWith show

-- | Throw an error of type @e@ with no display function.
--
-- @since 2.4.0.0
throwError_
:: forall e es a. (HasCallStack, Error e :> es)
=> e
-- ^ The error.
-> Eff es a
throwError e = unsafeEff $ \es -> do
Error eid <- getEnv @(Error e) es
throwIO $ ErrorWrapper eid callStack (toAny e)
throwError_ = withFrozenCallStack throwErrorWith (const "<opaque>")

-- | Handle an error of type @e@.
catchError
Expand Down Expand Up @@ -223,21 +248,25 @@ tryHandler
-> IO r
-> IO r
tryHandler ex eid0 handler next = case fromException ex of
Just (ErrorWrapper eid cs e)
Just (ErrorWrapper eid cs _ e)
| eid0 == eid -> pure $ handler cs (fromAny e)
| otherwise -> next
Nothing -> next

data ErrorWrapper = ErrorWrapper !ErrorId CallStack Any
data ErrorWrapper = ErrorWrapper !ErrorId CallStack String Any

instance Show ErrorWrapper where
showsPrec p (ErrorWrapper _ cs _)
= ("Effectful.Error.Static.ErrorWrapper\n\n" ++)
. showsPrec p (prettyCallStack cs)
showsPrec _ (ErrorWrapper _ cs errRep _)
= ("Effectful.Error.Static.ErrorWrapper: " ++)
. (errRep ++)
. ("\n" ++)
. (prettyCallStack cs ++)

instance Exception ErrorWrapper

catchErrorIO :: ErrorId -> IO a -> (CallStack -> e -> IO a) -> IO a
catchErrorIO eid m handler = do
m `catch` \err@(ErrorWrapper etag cs e) -> do
m `catch` \err@(ErrorWrapper etag cs _ e) -> do
if eid == etag
then handler cs (fromAny e)
else throwIO err
28 changes: 26 additions & 2 deletions effectful-core/src/Effectful/Labeled/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ module Effectful.Labeled.Error
, runErrorNoCallStackWith

-- ** Operations
, throwErrorWith
, throwError
, throwError_
, catchError
, handleError
, tryError
Expand Down Expand Up @@ -68,14 +70,36 @@ runErrorNoCallStackWith
-> Eff es a
runErrorNoCallStackWith = runLabeled @label . E.runErrorNoCallStackWith

-- | Throw an error of type @e@.
-- | Throw an error of type @e@ and specify a display function in case a
-- third-party code catches the internal exception and 'show's it.
throwErrorWith
:: forall label e es a
. (HasCallStack, Labeled label (Error e) :> es)
=> (e -> String)
-- ^ The display function.
-> e
-- ^ The error.
-> Eff es a
throwErrorWith display =
withFrozenCallStack send . Labeled @label . ThrowErrorWith display

-- | Throw an error of type @e@ with 'show' as a display function.
throwError
:: forall label e es a
. (HasCallStack, Labeled label (Error e) :> es, Show e)
=> e
-- ^ The error.
-> Eff es a
throwError = withFrozenCallStack (throwErrorWith @label) show

-- | Throw an error of type @e@ with no display function.
throwError_
:: forall label e es a
. (HasCallStack, Labeled label (Error e) :> es)
=> e
-- ^ The error.
-> Eff es a
throwError e = withFrozenCallStack $ send (Labeled @label $ ThrowError e)
throwError_ = withFrozenCallStack (throwErrorWith @label) (const "<opaque>")

-- | Handle an error of type @e@.
catchError
Expand Down
2 changes: 1 addition & 1 deletion effectful-plugin/tests/PluginTests.hs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ oStrState = put "hello"
err :: Error e :> es => Eff es Bool
err =
catchError
(throwError (error ""))
(throwError_ (error ""))
(\_ _ -> pure True)

errState :: (Num s, Error e :> es, State s :> es) => Eff es Bool
Expand Down
12 changes: 10 additions & 2 deletions effectful/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
* Improve `Effectful.Labeled`, add `Effectful.Labeled.Error`,
`Effectful.Labeled.Reader`, `Effectful.Labeled.State` and
`Effectful.Labeled.Writer`.
* **Breaking change**: `localSeqLend`, `localLend`, `localSeqBorrow` and
`localBorrow` now take a list of effects instead of a single one.
* Add `throwErrorWith` and `throwError_` to `Effectful.Error.Static` and
`Effectful.Error.Dynamic`.
* **Breaking changes**:
- `localSeqLend`, `localLend`, `localSeqBorrow` and `localBorrow` now take a
list of effects instead of a single one.
- `Effectful.Error.Static.throwError` now requires the error type to have a
`Show` constraint. If this is not the case for some of your error types, use
`throwError_` for them.
- `ThrowError` operation from the dynamic version of the `Error` effect was
replaced with `ThrowErrorWith`.

# effectful-2.3.1.0 (2024-06-07)
* Drop support for GHC 8.8.
Expand Down

0 comments on commit a022b49

Please sign in to comment.