diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,9 @@
 # Changelog
 
+- 0.3.0 (2025-06-21)
+  * The 'derive' function is now total, returning 'Nothing' when
+    supplied with bad inputs.
+
 - 0.2.1 (2025-02-24)
   * Merely swaps out the base16 library used for the Wycheproof tests
     (in favour of ppad-base16).
diff --git a/lib/Crypto/KDF/HMAC.hs b/lib/Crypto/KDF/HMAC.hs
--- a/lib/Crypto/KDF/HMAC.hs
+++ b/lib/Crypto/KDF/HMAC.hs
@@ -66,10 +66,10 @@
   -> BS.ByteString  -- ^ optional context and application-specific info
   -> Word64         -- ^ bytelength of output keying material
   -> BS.ByteString  -- ^ pseudorandom key
-  -> BS.ByteString  -- ^ output keying material
+  -> Maybe BS.ByteString  -- ^ output keying material
 expand (HMACEnv hmac hashlen) info (fi -> len) prk
-    | len > 255 * hashlen = error "ppad-hkdf (expand): invalid outlength"
-    | otherwise = BS.take len (go (1 :: Int) mempty mempty)
+    | len > 255 * hashlen = Nothing
+    | otherwise = pure (BS.take len (go (1 :: Int) mempty mempty))
   where
     n = ceiling ((fi len :: Double) / (fi hashlen :: Double)) :: Int
     go !j t !tl
@@ -95,7 +95,7 @@
   -> BS.ByteString -- ^ optional context and application-specific info
   -> Word64        -- ^ bytelength of output keying material (<= 255 * hashlen)
   -> BS.ByteString -- ^ input keying material
-  -> BS.ByteString -- ^ output keying material
+  -> Maybe BS.ByteString -- ^ output keying material
 derive hmac salt info len = expand env info len . extract env salt where
   env = HMACEnv hmac (fi (BS.length (hmac mempty mempty)))
 
diff --git a/ppad-hkdf.cabal b/ppad-hkdf.cabal
--- a/ppad-hkdf.cabal
+++ b/ppad-hkdf.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ppad-hkdf
-version:            0.2.1
+version:            0.3.0
 synopsis:           A HMAC-based key derivation function
 license:            MIT
 license-file:       LICENSE
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -3,11 +3,9 @@
 
 module Main where
 
-import Control.Exception
 import qualified Crypto.Hash.SHA256 as SHA256
 import qualified Crypto.Hash.SHA512 as SHA512
 import qualified Crypto.KDF.HMAC as KDF
-import qualified Data.ByteString as BS
 import qualified Data.Aeson as A
 import qualified Data.Text.IO as TIO
 import Test.Tasty
@@ -50,16 +48,13 @@
         inf = ht_info
         siz = ht_size
         pec = ht_okm
-    if   ht_result == "invalid"
-    then do
-      out <- try (pure $! KDF.derive hmac sal inf siz ikm)
-               :: IO (Either ErrorCall BS.ByteString)
-      case out of
-        Left _  -> assertBool "invalid" True
-        Right o -> assertBool "invalid" (pec /= o)
-    else do
-      let out = KDF.derive hmac sal inf siz ikm
-      assertEqual mempty pec out
+    case KDF.derive hmac sal inf siz ikm of
+      Nothing
+        | ht_result == "invalid" -> assertBool "invalid" True
+        | otherwise -> assertFailure "failed"
+      Just out
+        | ht_result == "invalid" -> assertBool "invalid" (pec /= out)
+        | otherwise -> assertEqual mempty pec out
   where
     hmac = case h of
       SHA256 -> SHA256.hmac
