diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Revision history for error
+
+## 0.1.0.0 -- 2021-06-26
+
+* Initial version. More or less feature complete, 
+  unless we decide we need extra helper functions.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2021 Profpatsch <mail@profpatsch.de>
+MIT LICENSE
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/error.cabal b/error.cabal
new file mode 100644
--- /dev/null
+++ b/error.cabal
@@ -0,0 +1,44 @@
+cabal-version:      2.4
+name:               error
+version:            0.1.0.0
+
+synopsis: The canonical error type
+
+-- A longer description of the package.
+description: A canonical `Error` type, which provides a way to turn an error string into an `Error`, add context to an `Error`, and pretty print the `Error` for displaying it to users.
+
+category: Data, Error Handling
+
+-- A URL where users can report bugs.
+homepage: https://github.com/Profpatsch/error
+bug-reports: https://github.com/Profpatsch/error/issues
+
+license: MIT
+license-file: LICENSE
+
+author: Profpatsch
+maintainer: Profpatsch <mail@profpatsch.de>
+copyright: 2021 Profpatsch
+
+extra-source-files:
+    CHANGELOG.md
+  , LICENSE
+
+library
+    hs-source-dirs:   src
+    default-language: Haskell2010
+    exposed-modules:
+        Data.Error
+    build-depends:
+        base >= 4.8.0.0 && < 5
+      , text >= 0.1
+
+    -- Modules included in this executable, other than Main.
+    -- other-modules:
+
+    -- LANGUAGE extensions used by modules in this package.
+    -- other-extensions:
+
+source-repository head
+  type: git
+  location: https://github.com/Profpatsch/error.git
diff --git a/src/Data/Error.hs b/src/Data/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Error.hs
@@ -0,0 +1,120 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Data.Error
+  ( Error,
+    newError,
+    addContext,
+    prettyError,
+    unwrapError,
+    expectError,
+  )
+where
+
+import Data.Function ((&))
+import Data.Text (Text)
+import qualified Data.Text as Text
+
+-- | 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@ ('addContext')
+-- * pretty-printed (`prettyError`)
+newtype Error = Error [Text]
+
+-- | Create an ad-hoc 'Error' from an error message.
+newError :: Text -> Error
+newError msg = Error [msg]
+
+-- | 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 'addContext',
+-- and describe why you wanted to open the file in the first place:
+--
+-- @
+-- addContext "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'.
+addContext :: Text -> Error -> Error
+addContext e (Error es) = Error $ e : es
+
+-- | 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
+--   $ addContext "Trying to open config file"
+--     $ newError "file not found: ./foo"
+--
+-- ==> "Trying to open config file: file not found: ./foo"
+-- @
+prettyError :: Error -> Text
+prettyError (Error es) = Text.intercalate ": " es
+
+-- | Return the value from a potentially failing computation.
+--
+-- Abort with the 'Error's message if it was a 'Left'.
+--
+-- Panic: if Error
+--
+-- Example:
+--
+-- @
+-- unwrapError $ Left (newError "oh no!")
+--
+-- ==> *** Exception: oh no!
+--
+-- unwrapError $ Right 42
+--
+-- ==> 42
+-- @
+unwrapError :: Either Error p -> p
+unwrapError e = case e of
+  Left err -> error (prettyError err & Text.unpack)
+  Right a -> a
+
+-- | 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.
+--
+-- Panic: if Error
+--
+-- Example:
+--
+-- @
+-- exceptError "something bad happened" $ Left (newError "oh no!")
+--
+-- ==> *** Exception: something bad happened: oh no!
+--
+-- exceptError $ Right 42
+--
+-- ==> 42
+-- @
+expectError :: Text -> Either Error p -> p
+expectError context e = case e of
+  Left err ->
+    error
+      ( err
+          & addContext context
+          & prettyError
+          & Text.unpack
+      )
+  Right a -> a
