Error handling
hash and verify return an Err branch of the Result type, carrying a BcryptError. This page covers what the variants mean and how to handle them.
The BcryptError type
pub type BcryptError =
| InvalidCost Int # cost was outside the allowed 4..31 range
| InvalidHash String # the hash string passed to verify was malformed
| HashError String # any other underlying failure (I/O, RNG, etc.)
deriving (Show, Eq, Debug)It derives Show, Eq, and Debug, so you can interpolate it in messages (via show/debug), compare values, and assert on them directly in tests.
When each variant occurs
InvalidCost n comes from hash, when the Config cost is outside bcrypt's valid range of 4 to 31. n is the offending value you passed. This is the only way hash fails in normal use:
hash "pw" (hasher |> with_cost 99) # Err (InvalidCost 99)
hash "pw" (hasher |> with_cost 2) # Err (InvalidCost 2)InvalidHash msg comes from verify, when the hash string can't be parsed — a bad prefix, wrong length, or invalid base64. msg is a human-readable description. This means the hash is broken, not that the password is wrong:
verify "pw" "not-a-real-hash" # Err (InvalidHash "Invalid hash: not-a-real-hash")HashError msg is the catch-all for anything else the underlying implementation reports (I/O, RNG). You are unlikely to see it in practice; handle it defensively as an unexpected internal failure.
Handling failures
Match on the Result and, where it matters, on the specific variant. A typical login check that logs when the stored hash is broken (the needs {Log} reflects the log! calls):
import Bcrypt (verify, InvalidHash, HashError)
fun check_login : String -> String -> Bool needs {Log}
check_login attempt stored = case verify attempt stored {
Ok matches -> matches # True or False — the real answer
Err (InvalidHash _) -> {
log! "stored hash is corrupted for this user"
False
}
Err (HashError msg) -> {
log! $"bcrypt internal error: {msg}"
False
}
}(If you don't need logging, drop the Err bodies down to plain False and the needs {Log} clause with them.)
Two things to keep straight:
Ok Falseis not an error. A wrong password is a successful verification that returnsFalse. Only reach for theErrarms when the hash itself is the problem. Collapsing the two loses a signal you want: a spike inErr (InvalidHash _)means your stored data is broken, whileOk Falseis just a bad login attempt.InvalidCostcan't come fromverify. Verification reads the cost out of the hash, so the only failure it produces isInvalidHash(or, rarely,HashError). You can safely omit anInvalidCostarm when matching on averifyresult — though a catch-allErr _is fine if you would rather not enumerate.
Testing against errors
Because BcryptError derives Eq and Debug, you can assert on exact errors:
import Std.Test (assert_eq)
import Bcrypt (hash, hasher, with_cost, InvalidCost)
assert_eq (hash "pw" (hasher |> with_cost 99)) (Err (InvalidCost 99))Or match the variant when the message is implementation-defined and you don't want to pin it:
case verify "pw" "garbage" {
Err (InvalidHash _) -> assert! True ""
other -> assert! False $"expected InvalidHash, got {debug other}"
}