Copyright | (C) 2021-2022 Profpatsch |
---|---|
License | MIT |
Maintainer | Profpatsch <mail@profpatsch.de> |
Stability | stable |
Portability | portable |
Safe Haskell | Safe-Inferred |
Language | Haskell2010 |
Synopsis
- data Error
- newError :: Text -> Error
- showToError :: Show a => a -> Error
- exceptionToError :: Exception exc => exc -> Error
- errorContext :: Text -> Error -> Error
- prettyError :: Error -> Text
- expectError :: HasCallStack => Text -> Either Error p -> p
- unwrapError :: HasCallStack => Either Error a -> a
- expectIOError :: Text -> Either Error a -> IO a
- unwrapIOError :: Either Error a -> IO a
- ifIOError :: Text -> IO a -> IO (Either Error a)
- ifError :: forall exc a. Exception exc => Text -> IO a -> IO (Either Error a)
Documentation
The canonical Error
type.
It can be
- created from a human-readable error message (
newError
) - more semantic context can be added to an existing
Error
(errorContext
) - pretty-printed (
prettyError
)
Instances
IsString Error Source # | This makes it possible to treat any literal string as
No |
Defined in Data.Error fromString :: String -> Error # | |
Show Error Source # | The If you want to display an error, use |
Error creation
From Show
and Exception
These two functions can be helpful, but consider that they don’t always provide very user-friendly error messages.
It is recommended that you use errorContext
to improve the messages generated by showToError
and exceptionToError
.
showToError :: Show a => a -> Error Source #
Create an error from a Show
type.
If your type implements Exception
, it is usually better to use exceptionToError
instead.
Strings produced by show
are usually not very user-friendly.
Note: goes via String
, so not efficient.
exceptionToError :: Exception exc => exc -> Error Source #
Create an error from an Exception
type.
The default implementation of displayException
is show
, so the same user-friendliness problems of showToError
apply.
Note: goes via String
, so not efficient.
Adding context
errorContext :: Text -> Error -> Error Source #
Add a higher-level context to an Error
.
For example, your code hits a “file not found” I/O exception.
Instead of propagating it unseen, you catch it and annotate it with errorContext
,
and describe why you wanted to open the file in the first place:
errorContext "Trying to open config file" $ newError "file not found: ./foo"
This way, when a user see the error, they will understand better what happened:
"Trying to open config file: file not found: ./foo"
See prettyError
.
Pretty printing
prettyError :: Error -> Text Source #
Pretty print the error.
It will print all context messages, starting with the outermost.
Example:
>>>
prettyError $ newError "file not found: ./foo"
"file not found: ./foo"
>>>
:{
prettyError $ errorContext "Trying to open config file" $ newError "file not found: ./foo" :} "Trying to open config file: file not found: ./foo"
Unsafe unwrapping
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 (via error
) if there was an error.
You can also use expectIOError
/unwrapIOError
if your code is in IO
,
which will crash with throwIO
instead of error
.
expectError
/expectIOError
should usually be preferred, since it adds a context message.
These are modelled after Result::expect()
and Result::unwrap()
in the rust stdlib.
expectError :: HasCallStack => Text -> Either Error p -> p Source #
Return the value from a potentially failing computation.
Abort with the error message if it was an error.
The text message is added to the Error
as additional context before aborting.
Panics: if Error
Example:
>>>
expectError "something bad happened" $ Left (newError "oh no!")
*** Exception: something bad happened: oh no! ...
>>>
expectError "something bad happened" $ Right 42
42
unwrapError :: HasCallStack => Either Error a -> a Source #
expectIOError :: Text -> Either Error a -> IO a Source #
Like expectError
, but instead of using error
, it will 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
(but no stack trace can be produced at the moment!).
Throws opaque Exception: if Error
Example:
>>>
expectIOError "something bad happened" $ Left (newError "oh no!")
*** Exception: 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 SomeException
, or it will bubble up and lead to
your program crashing.
unwrapIOError :: Either Error a -> IO a Source #
Like unwrapError
, but instead of using error
, it will 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
(but no stack trace can be produced at the moment!).
Throws opaque Exception: if Error
Example:
>>>
unwrapIOError $ Left (newError "oh no!")
*** Exception: 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 SomeException
, or it will bubble up and lead to
your program crashing.
Catching Exceptions
in IO
ifIOError :: Text -> IO a -> IO (Either Error a) Source #
Catch any 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 errorContext
. 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
*** Exception: could not open file: user error (oh noes!)
ifError :: forall exc a. Exception exc => Text -> IO a -> IO (Either Error a) Source #
Catch any 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 errorContext
. This prevents the common pattern of bubbling up exceptions
without any context.
Use TypeApplications
to specify the Exception
you want to catch.
>>>
:set -XTypeApplications
>>>
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).