error-1.0.0.0: The canonical error type
Copyright(C) 2021-2022 Profpatsch
LicenseMIT
MaintainerProfpatsch <mail@profpatsch.de>
Stabilitystable
Portabilityportable
Safe HaskellSafe-Inferred
LanguageHaskell2010

Data.Error

Description

 
Synopsis

Documentation

data Error Source #

The canonical Error type.

It can be

Instances

Instances details
IsString Error Source #

This makes it possible to treat any literal string as Error (with OverloadedStrings enabled).

>>> prettyError $ errorContext "oops" $ "my Error"
"oops: my Error"

No newError necessary!

Instance details

Defined in Data.Error

Methods

fromString :: String -> Error #

Show Error Source #

The Show instance exists for the user’s convenience on the REPL.

If you want to display an error, use prettyError instead.

Instance details

Defined in Data.Error

Methods

showsPrec :: Int -> Error -> ShowS #

show :: Error -> String #

showList :: [Error] -> ShowS #

Error creation

newError :: Text -> Error Source #

Create an ad-hoc Error from an error message.

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 #

Return the value from a potentially failing computation.

Abort with the Errors message if it was a Left.

Panics: if Error

Example:

>>> unwrapError $ Left (newError "oh no!")
*** Exception: oh no!
...
>>> unwrapError $ Right 42
42

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 IOExceptions 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 Exceptions 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).