diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,14 @@
+0.3.0.0
+
+* Replaced the `HoistError` typeclass, which is about monads, with a
+  simpler `PluckError` typeclass that is about extracting errors from
+  values.
+* Removed the instance for `Except e`, as it triggers overlapping
+  instance errors when attempting to hoist `ExceptT e m` for unknown
+  monads `m`.
+* Introduced a parallel `Control.Monad.Fail.Hoist` module, for
+  hoisting error messages into `MonadFail`.
+
 0.2.1.0
 
 * Removed unicode syntax and variables
diff --git a/hoist-error.cabal b/hoist-error.cabal
--- a/hoist-error.cabal
+++ b/hoist-error.cabal
@@ -1,5 +1,5 @@
 name:                hoist-error
-version:             0.2.1.0
+version:             0.3.0.0
 synopsis:            Some convenience facilities for hoisting errors into a monad
 description:         Provides a typeclass and useful combinators for hoisting errors into a monad.
 license:             MIT
@@ -10,27 +10,27 @@
 category:            Control
 build-type:          Simple
 cabal-version:       >=1.10
-tested-with:         GHC == 7.8.4, GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.1
 
 extra-source-files:  changelog.md
 
-tested-with:         GHC == 7.8.4
-                   , GHC == 7.10.3
-                   , GHC == 8.0.2
-                   , GHC == 8.2.2
-                   , GHC == 8.4.1
-
-
+tested-with:         GHC == 8.8.4
+                   , GHC == 8.10.7
+                   , GHC == 9.0.1
+                   , GHC == 9.2.7
+                   , GHC == 9.4.5
+                   , GHC == 9.6.4
+                   , GHC == 9.8.2
 
 source-repository head
     type: git
     location: https://github.com/alephcloud/hs-hoist-error.git
 
 library
-  exposed-modules:     Control.Monad.Error.Hoist
-  build-depends:       base   >=4.7 && <4.12
-                     , mtl    >=2.1 && <2.3
-                     , either >=4   && <6
+  exposed-modules:
+    Control.Monad.Error.Hoist
+    Control.Monad.Fail.Hoist
+  build-depends:       base   >=4.13 && <4.20
+                     , mtl    >=2.2 && <2.4
 
   hs-source-dirs:      src
   default-language:    Haskell2010
diff --git a/src/Control/Monad/Error/Hoist.hs b/src/Control/Monad/Error/Hoist.hs
--- a/src/Control/Monad/Error/Hoist.hs
+++ b/src/Control/Monad/Error/Hoist.hs
@@ -1,44 +1,50 @@
-{-# LANGUAGE CPP                    #-}
 {-# LANGUAGE FlexibleInstances      #-}
+{-# LANGUAGE DefaultSignatures      #-}
 {-# LANGUAGE FunctionalDependencies #-}
 {-# LANGUAGE GADTs                  #-}
-{-# LANGUAGE MultiParamTypeClasses  #-}
+{-# LANGUAGE TypeOperators          #-}
+{-# LANGUAGE UndecidableInstances   #-}
 
--- | 'HoistError' extends 'MonadError' with 'hoistError', which enables lifting
--- of partiality types such as 'Maybe' and @'Either' e@ into the monad.
+-- | This module provides helper functions for lifting partiality types into
+-- error-carrying monads like 'ExceptT'.
 --
 -- For example, consider the following @App@ monad that may throw @BadPacket@
 -- errors:
 --
 -- @
--- data AppError = BadPacket 'String'
+-- data AppError = BadPacket 'Text'
 --
 -- newtype App a = App ('EitherT' AppError 'IO') a
 --  deriving ('Functor', 'Applicative', 'Monad', 'MonadError' AppError, 'MonadIO')
 -- @
 --
--- We may have an existing function that parses a 'String' into a @'Maybe' Packet@
+-- We may have an existing function that attempts to parse a 'ByteString':
 --
 -- @
--- parsePacket :: 'String' -> 'Maybe' Packet
+-- parsePacket :: 'ByteString' -> 'Either' 'Text' Packet
 -- @
 --
--- which can be lifted into the @App@ monad with 'hoistError'
+-- We can lift this error into the @App@ monad using @('<%?>')@:
 --
 -- @
--- appParsePacket :: 'String' -> 'App' Packet
--- appParsePacket s = 'hoistError' (\\() -> BadPacket "no parse") (parsePacket s)
+-- appParsePacket :: 'ByteString' -> 'App' Packet
+-- appParsePacket s = parsePacket s \<%?\> BadPacket
 -- @
 --
--- Similar instances exist for @'Either' e@ and @'EitherT' e m@.
+-- Instances also exist for extracting errors from other partiality types
+-- like @'Either' e@ and @'ExceptT' e m@.
 
 module Control.Monad.Error.Hoist
-  ( HoistError(..)
+  ( hoistError
   , hoistErrorM
+  -- ** Operators
+  -- $mnemonics
   , (<%?>)
   , (<%!?>)
   , (<?>)
   , (<!?>)
+  -- * Helper class
+  , PluckError(..)
   ) where
 
 import           Control.Monad              ((<=<))
@@ -46,63 +52,23 @@
 
 import           Data.Either                (Either, either)
 
-#if MIN_VERSION_mtl(2,2,2)
 import           Control.Monad.Except       (Except, ExceptT, runExcept,
                                              runExceptT)
-#else
-import           Control.Monad.Error        (Error, ErrorT, runErrorT)
-#endif
 
-#if MIN_VERSION_either(5,0,0)
--- Control.Monad.Trans.Either was removed from @either@ in version 5.
-#else
-import           Control.Monad.Trans.Either (EitherT, eitherT, runEitherT)
-#endif
-
--- | A tricky class for easily hoisting errors out of partiality types (e.g.
--- 'Maybe', @'Either' e@) into a monad. The parameter @e@ represents the error
--- information carried by the partiality type @t@, and @e'@ represents the type
--- of error expected in the monad @m@.
+-- | Given a conversion from the error in @t a@ to @e'@, we can hoist the
+-- computation into @m@.
 --
-class Monad m => HoistError m t e e' | t -> e where
-
-  -- | Given a conversion from the error in @t a@ to @e'@, we can hoist the
-  -- computation into @m@.
-  --
-  -- @
-  -- 'hoistError' :: 'MonadError' e m -> (() -> e) -> 'Maybe'       a -> m a
-  -- 'hoistError' :: 'MonadError' e m -> (a  -> e) -> 'Either'  a   b -> m b
-  -- 'hoistError' :: 'MonadError' e m -> (a  -> e) -> 'ExceptT' a m b -> m b
-  -- @
-  hoistError
-    :: (e -> e')
-    -> t a
-    -> m a
-
-instance MonadError e m => HoistError m Maybe () e where
-  hoistError f = maybe (throwError $ f ()) return
-
-instance MonadError e' m => HoistError m (Either e) e e' where
-  hoistError f = either (throwError . f) return
-
-#if MIN_VERSION_either(5,0,0)
--- Control.Monad.Trans.Either was removed from @either@ in version 5.
-#else
-instance (m ~ n, MonadError e' m) => HoistError m (EitherT e n) e e' where
-  hoistError f = eitherT (throwError . f) return
-#endif
-
-#if MIN_VERSION_mtl(2,2,2)
-instance MonadError e' m => HoistError m (Except e) e e' where
-  hoistError f = either (throwError . f) return . runExcept
-
-instance MonadError e' m => HoistError m (ExceptT e m) e e' where
-  hoistError f = either (throwError . f) return <=< runExceptT
-#else
--- 'ErrorT' was renamed to 'ExceptT' in mtl 2.2.2
-instance MonadError e' m => HoistError m (ErrorT e m) e e' where
-  hoistError f = either (throwError . f) return <=< runErrorT
-#endif
+-- @
+-- 'hoistError' :: 'MonadError' e m -> (() -> e) -> 'Maybe'       a -> m a
+-- 'hoistError' :: 'MonadError' e m -> (a  -> e) -> 'Either'  a   b -> m b
+-- 'hoistError' :: 'MonadError' e m -> (a  -> e) -> 'ExceptT' a m b -> m b
+-- @
+hoistError
+  :: (PluckError e t m, MonadError e' m)
+  => (e -> e')
+  -> t a
+  -> m a
+hoistError f = foldError (throwError . f) pure
 
 -- | A version of 'hoistError' that operates on values already in the monad.
 --
@@ -112,14 +78,28 @@
 -- 'hoistErrorM' :: 'MonadError' e m => (a  -> e) ->    'ExceptT' a m b  -> 'ExceptT' a m b
 -- @
 hoistErrorM
-  :: HoistError m t e e'
+  :: (PluckError e t m, MonadError e' m)
   => (e -> e')
   -> m (t a)
   -> m a
-hoistErrorM e m = do
-  x <- m
-  hoistError e x
+hoistErrorM e m = m >>= hoistError e
 
+-- $mnemonics
+--
+-- The operators in this package are named according to a scheme:
+--
+-- * @('<?>')@ is the simplest error-handling function: it replaces
+--   any error with its second argument.
+--
+-- * The additional @!@ in @('<!?>')@ and @('<%!?>')@ means the
+--   operator handles values that are already "in a monad".
+--
+-- * The additional @%@ in @('<%?>')@ and @('<%!?>')@ means the
+--   operator takes a function argument, which it applies to the error
+--   from the partiality type. (The mnemonic is that @%@ sometimes
+--   means "mod", and we abuse "mod" as a shorthand for "modify". It's a
+--   long bow, but @lens@ uses the same mnemonic.)
+
 -- | A flipped synonym for 'hoistError'.
 --
 -- @
@@ -128,7 +108,7 @@
 -- ('<%?>') :: 'MonadError' e m => 'ExceptT' a m b -> (a  -> e) -> 'ExceptT' a m b
 -- @
 (<%?>)
-  :: HoistError m t e e'
+  :: (PluckError e t m, MonadError e' m)
   => t a
   -> (e -> e')
   -> m a
@@ -145,7 +125,7 @@
 -- ('<%!?>') :: 'MonadError' e m =>    'ExceptT' a m b  -> (a  -> e) -> 'ExceptT' a m b
 -- @
 (<%!?>)
-  :: HoistError m t e e'
+  :: (PluckError e t m, MonadError e' m)
   => m (t a)
   -> (e -> e')
   -> m a
@@ -163,7 +143,7 @@
 -- ('<?>') :: 'MonadError' e m => 'ExceptT' a m b -> e -> 'ExceptT' a m b
 -- @
 (<?>)
-  :: HoistError m t e e'
+  :: (PluckError e t m, MonadError e' m)
   => t a
   -> e'
   -> m a
@@ -180,7 +160,7 @@
 -- ('<!?>') :: 'MonadError' e m =>    'ExceptT' a m b  -> e -> 'ExceptT' a m b
 -- @
 (<!?>)
-  :: HoistError m t e e'
+  :: (PluckError e t m, MonadError e' m)
   => m (t a)
   -> e'
   -> m a
@@ -190,3 +170,27 @@
 
 infixl 8 <!?>
 {-# INLINE (<!?>) #-}
+
+-- | A class for plucking an error @e@ out of a partiality type @t@.
+class PluckError e t m | t -> e where
+  pluckError :: t a -> m (Either e a)
+  default pluckError :: Applicative m => t a -> m (Either e a)
+  pluckError = foldError (pure . Left) (pure . Right)
+
+  foldError :: (e -> m r) -> (a -> m r) -> t a -> m r
+  default foldError :: Monad m => (e -> m r) -> (a -> m r) -> t a -> m r
+  foldError f g = either f g <=< pluckError
+
+  {-# MINIMAL pluckError | foldError #-}
+
+instance (Applicative m, e ~ ()) => PluckError e Maybe m where
+  pluckError = pure . maybe (Left ()) Right
+  foldError f = maybe (f ())
+
+instance Applicative m => PluckError e (Either e) m where
+  pluckError = pure
+  foldError = either
+
+instance Monad m => PluckError e (ExceptT e m) m where
+  pluckError = runExceptT
+  foldError f g = either f g <=< runExceptT
diff --git a/src/Control/Monad/Fail/Hoist.hs b/src/Control/Monad/Fail/Hoist.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Monad/Fail/Hoist.hs
@@ -0,0 +1,140 @@
+{-# LANGUAGE FlexibleContexts #-}
+
+-- | This module provides helpers for converting partiality types into
+-- 'MonadFail' computations.
+--
+-- 'MonadFail''s purpose is to handle pattern-match failures in
+-- @do@-expressions, and not to be a general-purpose error-handling
+-- mechanism. Despite this, some libraries use it as one, and this
+-- module can help you report errors via 'MonadFail'.
+--
+-- The operator mnemonics are the same as in
+-- "Control.Monad.Error.Hoist", but with @#@ in place of @?@. You can
+-- imagine a hastily-written @F@ looking kinda-sorta like a @#@, if it
+-- helps.
+
+module Control.Monad.Fail.Hoist
+  ( hoistFail
+  , hoistFail'
+  , hoistFailM
+  , hoistFailM'
+  -- ** Operators
+  , (<%#>)
+  , (<%!#>)
+  , (<#>)
+  , (<!#>)
+  ) where
+
+import           Control.Monad.Error.Hoist  (PluckError(..))
+
+-- | Given a conversion from the error in @t a@ to @String@, we can hoist the
+-- computation into @m@.
+--
+-- @
+-- 'hoistFail' :: 'MonadFail' m => (() -> String) -> 'Maybe'    a -> m a
+-- 'hoistFail' :: 'MonadFail' m => (a  -> String) -> 'Either' a b -> m b
+-- @
+hoistFail
+  :: (PluckError e t m, MonadFail m)
+  => (e -> String)
+  -> t a
+  -> m a
+hoistFail f = foldError (fail . f) pure
+
+-- | Hoist computations whose error type is already 'String'.
+hoistFail' :: (PluckError String t m, MonadFail m) => t a -> m a
+hoistFail' = hoistFail id
+
+-- | A version of 'hoistFail' that operates on values already in the monad.
+--
+-- @
+-- 'hoistFailM' :: 'MonadFail' m => (() -> String) -> m ('Maybe'       a) ->           m a
+-- 'hoistFailM' :: 'MonadFail' m => (a  -> String) -> m ('Either'  a   b) ->           m b
+-- 'hoistFailM' :: 'MonadFail' m => (a  -> String) ->    'ExceptT' a m b  -> 'ExceptT' a m b
+-- @
+hoistFailM
+  :: (PluckError e t m, MonadFail m)
+  => (e -> String)
+  -> m (t a)
+  -> m a
+hoistFailM f m = m >>= hoistFail f
+
+-- | A version of 'hoistFail'' that operates on values already in the monad.
+--
+-- @
+-- 'hoistFailM'' :: 'MonadFail' m => m ('Maybe'       a) ->           m a
+-- 'hoistFailM'' :: 'MonadFail' m => m ('Either'  a   b) ->           m b
+-- 'hoistFailM'' :: 'MonadFail' m =>    'ExceptT' a m b  -> 'ExceptT' a m b
+-- @
+hoistFailM'
+  :: (PluckError String t m, MonadFail m)
+  => m (t a)
+  -> m a
+hoistFailM' = hoistFailM id
+
+-- | A flipped synonym for 'hoistFail'. Mnemonic: @#@ looks a bit like @F@.
+--
+-- @
+-- ('<%#>') :: 'MonadFail' m => 'Maybe'    a -> (() -> e) -> m a
+-- ('<%#>') :: 'MonadFail' m => 'Either' a b -> (a  -> e) -> m b
+-- @
+(<%#>)
+  :: (PluckError e t m, MonadFail m)
+  => t a
+  -> (e -> String)
+  -> m a
+(<%#>) = flip hoistFail
+
+infixl 8 <%#>
+{-# INLINE (<%#>) #-}
+
+-- | A flipped synonym for 'hoistFailM'.
+--
+-- @
+-- ('<%!#>') :: 'MonadError' e m => m ('Maybe'       a) -> (() -> e) ->           m a
+-- ('<%!#>') :: 'MonadError' e m => m ('Either'  a   b) -> (a  -> e) ->           m b
+-- ('<%!#>') :: 'MonadError' e m =>    'ExceptT' a m b  -> (a  -> e) -> 'ExceptT' a m b
+-- @
+(<%!#>)
+  :: (PluckError e t m, MonadFail m)
+  => m (t a)
+  -> (e -> String)
+  -> m a
+(<%!#>) = flip hoistFailM
+
+infixl 8 <%!#>
+{-# INLINE (<%!#>) #-}
+
+-- | A version of '<%#>' that ignores the error in @t a@ and fails
+-- with a new one.
+--
+-- @
+-- ('<#>') :: 'MonadFail' m => 'Maybe'    a -> String -> m a
+-- ('<#>') :: 'MonadFail' m => 'Either' a b -> String -> m b
+-- @
+(<#>)
+  :: (PluckError e t m, MonadFail m)
+  => t a
+  -> String
+  -> m a
+m <#> e = m <%#> const e
+
+infixl 8 <#>
+{-# INLINE (<#>) #-}
+
+-- | A version of '<#>' that operates on values already in the monad.
+--
+-- @
+-- ('<!#>') :: 'MonadFail m => m ('Maybe'       a) -> String ->           m a
+-- ('<!#>') :: 'MonadFail m => m ('Either'  a   b) -> String ->           m b
+-- ('<!#>') :: 'MonadFail m =>    'ExceptT' a m b  -> String -> 'ExceptT' a m b
+-- @
+(<!#>)
+  :: (PluckError e t m, MonadFail m)
+  => m (t a)
+  -> String
+  -> m a
+m <!#> e = m >>= hoistFail (const e)
+
+infixl 8 <!#>
+{-# INLINE (<!#>) #-}
