error 0.2.0.0 → 0.2.1.0
raw patch · 3 files changed
+128/−6 lines, 3 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ Data.Error: expectIOError :: Text -> Either Error a -> IO a
+ Data.Error: ifError :: forall exc a. Exception exc => Text -> IO a -> IO (Either Error a)
+ Data.Error: ifIOError :: Text -> IO a -> IO (Either Error a)
+ Data.Error: instance GHC.Exception.Type.Exception Data.Error.ErrorException
+ Data.Error: instance GHC.Show.Show Data.Error.ErrorException
+ Data.Error: unwrapIOError :: Either Error a -> IO a
- Data.Error: unwrapError :: HasCallStack => Either Error p -> p
+ Data.Error: unwrapError :: HasCallStack => Either Error a -> a
Files
- CHANGELOG.md +5/−0
- error.cabal +1/−1
- src/Data/Error.hs +122/−5
CHANGELOG.md view
@@ -1,5 +1,10 @@ # Revision history for error +## 0.2.1.0 -- 2021-11-13++* Add `expectErrorIO` and `unwrapErrorIO`.+* Add `ifIOError` and `ifError`.+ ## 0.2.0.0 -- 2021-10-29 * Add `Show` instance to `Error`.
error.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: error-version: 0.2.0.0+version: 0.2.1.0 synopsis: The canonical error type
src/Data/Error.hs view
@@ -1,5 +1,10 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE ExplicitForAll #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE LambdaCase #-} module Data.Error ( Error, -- * Error creation@@ -18,15 +23,24 @@ -- | Sometimes you want to assure that an 'Error' could not have happened at runtime, -- even though there is the possibility in the types.+ -- -- In that case you can use 'expectError'/'unwrapError'.- -- They will panic at runtime if there was an error.+ -- They will panic at runtime (via 'error') if there was an error. --- -- 'expectError' should usually be preffered, since it adds a context message.+ -- You can also use 'expectIOError'/'unwrapIOError' if your code is in 'IO',+ -- which will crash with 'Exc.throwIO' instead of 'error'. --+ -- 'expectError'/'expectIOError' should usually be preferred, since it adds a context message.+ -- -- These are modelled after @<https://doc.rust-lang.org/std/result/enum.Result.html#method.expect Result::expect()>@ -- and @<https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap Result::unwrap()>@ in the rust stdlib. expectError, unwrapError,+ expectIOError,+ unwrapIOError,+ -- * Catching @Exceptions@ in 'IO'+ ifIOError,+ ifError ) where @@ -35,6 +49,9 @@ import qualified Data.Text as Text import GHC.Stack (HasCallStack) import Control.Exception (Exception (displayException))+import qualified Control.Exception as Exc+import Data.Functor ((<&>))+import Data.Bifunctor (first) -- | The canonical @Error@ type. --@@ -114,7 +131,7 @@ -- -- Abort with the 'Error's message if it was a 'Left'. ----- __Panics__: if Error+-- __Panics:__ if Error -- -- Example: --@@ -124,7 +141,7 @@ -- -- >>> unwrapError $ Right 42 -- 42-unwrapError :: HasCallStack => Either Error p -> p+unwrapError :: HasCallStack => Either Error a -> a unwrapError e = case e of Left err -> error (prettyError err & Text.unpack) Right a -> a@@ -135,7 +152,7 @@ -- -- The text message is added to the 'Error' as additional context before aborting. ----- __Panics__: if Error+-- __Panics:__ if Error -- -- Example: --@@ -155,3 +172,103 @@ & Text.unpack ) Right a -> a++-- | This Exception is not exported so that it’s impossible to catch and handle via 'Typeable'.+newtype ErrorException = ErrorException Error++-- | Show the pretty printed string without quotes.+instance Show ErrorException where+ showsPrec i (ErrorException err) = showString (err & prettyError & Text.unpack)+instance Exception ErrorException where+ displayException = show++-- | Like 'unwrapError', but instead of using 'error', it will 'Exc.throwIO' the pretty-printed error.+--+-- The advantage over `unwrapError` is that it crashes immediately, and not just when the 'Either' is forced,+-- leading to a deterministic immediate abortion of your IO code+-- (<https://github.com/ghc-proposals/ghc-proposals/pull/330 but no stack trace can be produced at the moment!>).+--+-- __Throws opaque Exception:__ if Error+--+-- Example:+--+-- >>> unwrapIOError $ Left (newError "oh no!")+-- oh no!+--+-- Important: 'Error' itself does /not/ implement 'Exception' to discourage the exception workflow.+-- The 'Exception' thrown is private to this module and thus can’t be “caught” in a typed manner.+-- If you use this function, you either have to catch 'Exc.SomeException', or it will bubble up and lead to+-- your program crashing.+unwrapIOError :: Either Error a -> IO a+unwrapIOError = \case+ Left err -> Exc.throwIO $ ErrorException err+ Right a -> pure a++-- | Like 'expectError', but instead of using 'error', it will 'Exc.throwIO' the pretty-printed error.+--+-- The advantage over `expectError` is that it crashes immediately, and not just when the 'Either' is forced,+-- leading to a deterministic immediate abortion of your IO code+-- (<https://github.com/ghc-proposals/ghc-proposals/pull/330 but no stack trace can be produced at the moment!>).+--+--+-- __Throws opaque Exception:__ if Error+--+-- Example:+--+-- >>> expectIOError "something bad happened" $ Left (newError "oh no!")+-- something bad happened: oh no!+--+-- Important: 'Error' itself does /not/ implement 'Exception' to discourage the exception workflow.+-- The 'Exception' thrown is private to this module and thus can’t be “caught” in a typed manner.+-- If you use this function, you either have to catch 'Exc.SomeException', or it will bubble up and lead to+-- your program crashing.+expectIOError :: Text -> Either Error a -> IO a+expectIOError context = \case+ Left err -> Exc.throwIO $ ErrorException (addContext context err)+ Right a -> pure a++-- | Catch any 'Exc.IOException's thrown by an @IO a@ as @Either Error a@.+--+-- The IOException is converted to an error with 'exceptionToError', and the given message+-- is added with 'addContext'. This prevents the common pattern of bubbling up exceptions+-- without any context.+--+-- >>> ifIOError "could not open file" (Control.Exception.throwIO (userError "oh noes!"))+-- Left (Error ["could not open file","user error (oh noes!)"])+--+-- It can then be handled like a normal 'Error'.+--+-- The function lends itself to piping with '(&)':+--+-- >>> Control.Exception.throwIO (userError "oh noes!") & ifIOError "could not open file"+-- Left (Error ["could not open file","user error (oh noes!)"])+--+-- and if you just want to annotate the error and directly throw it again:+--+-- >>> Control.Exception.throwIO (userError "oh noes!") & ifIOError "could not open file" >>= unwrapIOError+-- could not open file: user error (oh noes!)+ifIOError :: Text -> IO a -> IO (Either Error a)+ifIOError = ifError @Exc.IOException++-- | Catch any 'Exc.Exception's thrown by an @IO a@ as @Either Error a@.+--+-- The IOException is converted to an error with 'exceptionToError', and the given message+-- is added with 'addContext'. This prevents the common pattern of bubbling up exceptions+-- without any context.+--+-- Use @TypeApplications@ to specify the 'Exception' you want to catch.+--+-- >>> ifError @Exc.ArithException "arithmetic exception" (putStr $ show $ 1 Data.Ratio.% 0)+-- Left (Error ["arithmetic exception","Ratio has zero denominator"])+--+-- It can then be handled like a normal 'Error'.+--+-- The function lends itself to piping with '(&)':+--+-- >>> (putStr $ show $ 1 Data.Ratio.% 0) & ifError @Exc.ArithException "arithmetic exception"+-- Left (Error ["arithmetic exception","Ratio has zero denominator"])+--+-- Bear in mind that pure exceptions are only raised when the resulting code is forced+-- (thus the @putStrLn $ show@ in the above example).+ifError :: forall exc a. (Exception exc) => Text -> IO a -> IO (Either Error a)+ifError context io = Exc.try @exc io <&> first (addContext context . exceptionToError)