diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Revision history for bytesmith
 
+## 0.3.10.0 -- 2023-07-25
+
+* Add `mapErrorEffectfully`.
+* Correct the implementation of `satisfy`.
+* Add `takeUpTo`.
+
 ## 0.3.9.1 -- 2022-12-06
 
 * Build with GHC 9.4.
diff --git a/bytesmith.cabal b/bytesmith.cabal
--- a/bytesmith.cabal
+++ b/bytesmith.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.2
 name: bytesmith
-version: 0.3.9.1
+version: 0.3.10.0
 synopsis: Nonresumable byte parser
 description:
   Parse bytes as fast as possible. This is a nonresumable parser
@@ -37,7 +37,7 @@
     , bytestring >=0.10.8 && <=0.12
     , byteslice >=0.2.6 && <0.3
     , contiguous >= 0.6 && < 0.7
-    , primitive >=0.7 && <0.8
+    , primitive >=0.7 && <0.9
     , text-short >=0.1.3 && <0.2
     , run-st >=0.1 && <0.2
     , wide-word >=0.1.0.9 && <0.2
diff --git a/src/Data/Bytes/Parser.hs b/src/Data/Bytes/Parser.hs
--- a/src/Data/Bytes/Parser.hs
+++ b/src/Data/Bytes/Parser.hs
@@ -37,6 +37,7 @@
   , any
     -- * Many Bytes
   , take
+  , takeUpTo
   , takeWhile
   , takeTrailedBy
     -- * Skip
@@ -66,6 +67,7 @@
   , orElse
   , annotate
   , (<?>)
+  , mapErrorEffectfully
     -- * Repetition
   , replicate
     -- * Subparsing
@@ -421,6 +423,16 @@
     bs -> Internal.Success bs (offset chunk + n) (length chunk - n)
   else Internal.Failure e
 
+-- | Take at most the given number of bytes. This is greedy. It will
+--   consume as many bytes as there are available until it has consumed
+--   @n@ bytes. This never fails.
+takeUpTo :: Int -> Parser e s Bytes
+{-# inline takeUpTo #-}
+takeUpTo n = uneffectful $ \chunk ->
+  let m = min n (B.length chunk)
+   in case B.unsafeTake m chunk of
+        bs -> Internal.Success bs (offset chunk + m) (length chunk - m)
+
 -- | Consume all remaining bytes in the input.
 remaining :: Parser e s Bytes
 {-# inline remaining #-}
@@ -457,8 +469,8 @@
 --   The parser returns the transformed byte that was parsed.
 satisfyWith :: e -> (Word8 -> a) -> (a -> Bool) -> Parser e s a
 {-# inline satisfyWith #-}
-satisfyWith e f p = uneffectful $ \chunk -> if length chunk > 1
-  then case B.unsafeIndex chunk 1 of
+satisfyWith e f p = uneffectful $ \chunk -> if length chunk > 0
+  then case B.unsafeIndex chunk 0 of
     w ->
       let v = f w
       in if p v
@@ -569,6 +581,18 @@
   (\x s0 -> case f x s0 of
     (# s1, r0 #) -> case r0 of
       (# _ | #) -> g x s1
+      (# | r #) -> (# s1, (# | r #) #)
+  )
+
+-- | Effectfully adjusts the error message if an error occurs.
+mapErrorEffectfully :: (e1 -> ST s e2) -> Parser e1 s a -> Parser e2 s a
+{-# inline mapErrorEffectfully #-}
+mapErrorEffectfully f (Parser g) = Parser
+  (\x s0 -> case g x s0 of
+    (# s1, r0 #) -> case r0 of
+      (# e | #) -> case f e of
+        ST h -> case h s1 of
+          (# s2, e' #) -> (# s2, (# e' | #) #)
       (# | r #) -> (# s1, (# | r #) #)
   )
 
diff --git a/src/Data/Bytes/Parser/Rebindable.hs b/src/Data/Bytes/Parser/Rebindable.hs
--- a/src/Data/Bytes/Parser/Rebindable.hs
+++ b/src/Data/Bytes/Parser/Rebindable.hs
@@ -1,12 +1,13 @@
+{-# language CPP #-}
+{-# language DataKinds #-}
 {-# language FlexibleInstances #-}
 {-# language MagicHash #-}
 {-# language MultiParamTypeClasses #-}
+{-# language PolyKinds #-}
 {-# language RankNTypes #-}
 {-# language ScopedTypeVariables #-}
-{-# language TypeInType #-}
 {-# language UnboxedSums #-}
 {-# language UnboxedTuples #-}
-{-# language CPP #-}
 
 -- | Provides levity-polymorphic variants of @>>=@, @>>@, and @pure@
 -- used to assemble parsers whose result types are unlifted. This
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -436,6 +436,12 @@
         ===
         Just w
     ]
+  , testGroup "satisfy"
+    [ testCase "A" $
+        P.Success (Slice 2 0 0x20)
+        @=?
+        P.parseBytes (P.satisfy () (== 0x20)) (bytes "\x20")
+    ]
   ]
 
 bytes :: String -> Bytes
