Error handling
Haskell’s error-handling story has two principal layers: value-level errors expressed through Maybe and Either (the conventional choice for ordinary domain failures), and exception-based errors through the Control.Exception machinery (for genuinely exceptional conditions and for IO-related failures). The two coexist, and choosing between them is one of the principal design decisions in any non-trivial Haskell program. Modern Haskell strongly favours value-level error handling for recoverable errors and reserves exceptions for cases where the alternative would be impractical.
This page covers the value-level error types, the exception machinery, the Control.Exception interface, the bracket and try patterns, and the conventional discipline.
Total functions
A total function is one that produces a value for every input. The conventional Haskell discipline is to write total functions whenever possible; partial functions (those that may fail to terminate or produce a runtime error on some inputs) are considered code smell.
The principal partial functions in base:
| Function | Partial because |
|---|---|
head, tail, init, last | Fails on [] |
(!!) | Fails on out-of-bounds index |
fromJust | Fails on Nothing |
read | Fails on unparseable input |
error | Always fails |
undefined | Always fails |
div, mod, quot, rem | Fail on division by zero |
Each has a total alternative:
| Partial | Total |
|---|---|
head | Data.Maybe.listToMaybe |
tail | Data.List.uncons (returns Maybe) |
(!!) | Pattern matching, or Data.List.find |
fromJust | Data.Maybe.fromMaybe, pattern match |
read | Text.Read.readMaybe |
div n 0 | Pattern match on the divisor |
The conventional advice: prefer the total form. Linters (Hlint) and idiomatic libraries flag uses of partial functions; the discipline catches the bug at the source.
Maybe for absence
Maybe represents the absence of a value:
data Maybe a = Nothing | Just a
lookup :: Eq k => k -> [(k, v)] -> Maybe v
parseInt :: String -> Maybe Int
findUser :: Int -> Maybe User
The conventional uses are search and parse functions where the failure mode is “the input doesn’t match the criterion” without further detail.
Common patterns:
import Data.Maybe
-- Provide a default:
fromMaybe :: a -> Maybe a -> a
greeting = fromMaybe "stranger" lookupName
-- Map over the Maybe:
maybe :: b -> (a -> b) -> Maybe a -> b
display = maybe "no value" show maybeNumber
-- Filter and collect:
catMaybes :: [Maybe a] -> [a]
mapMaybe :: (a -> Maybe b) -> [a] -> [b]
The Maybe monad admits chaining fallible operations:
processInput :: String -> Maybe Result
processInput s = do
parsed <- parseInput s
validated <- validateInput parsed
enriched <- enrichWith parsed validated
return (computeResult enriched)
The chain produces Nothing on the first failure; the rest is short-circuited. The treatment is in Monads.
Either for value-or-error
Either carries an error value:
data Either a b = Left a | Right b
By convention:
Leftis the error case.Rightis the success case.
(Mnemonic: right is correct.)
The conventional use is for fallible operations where the failure mode warrants explanation:
parseConfig :: String -> Either ConfigError Config
loadFile :: FilePath -> IO (Either IOError Text)
processOrder :: Order -> Either ValidationError Result
The Either e monad admits chaining:
processOrder :: Input -> Either String Result
processOrder input = do
raw <- parseInput input
valid <- validateOrder raw
enriched <- enrichOrderData valid
return (computeResult enriched)
The chain produces “the first error” or the final result.
Common functions:
import Data.Either
either :: (a -> c) -> (b -> c) -> Either a b -> c
fromRight :: b -> Either a b -> b
isLeft :: Either a b -> Bool
isRight :: Either a b -> Bool
lefts :: [Either a b] -> [a] -- collect all errors
rights :: [Either a b] -> [b] -- collect all successes
partitionEithers :: [Either a b] -> ([a], [b])
For substantial applications, library types like Validation from the validation package admit accumulating errors (as opposed to short-circuiting on the first):
import Data.Validation
result :: Validation [String] Result
result = mkResult <$> validateName name <*> validateAge age
-- if both fail, the error list contains both messages
The Validation is an Applicative (not a Monad); the construction admits collecting all errors rather than just the first.
Exceptions
For non-recoverable failures and for IO-related errors that don’t admit value-level treatment, Haskell provides exceptions:
import Control.Exception
throwIO :: Exception e => e -> IO a
catch :: Exception e => IO a -> (e -> IO a) -> IO a
try :: Exception e => IO a -> IO (Either e a)
handle :: Exception e => (e -> IO a) -> IO a -> IO a
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
finally :: IO a -> IO b -> IO a
The Exception class admits any type:
data MyException = MyException String
deriving (Show)
instance Exception MyException
main = do
throwIO (MyException "something went wrong")
`catch` \(MyException msg) -> putStrLn ("Caught: " ++ msg)
The convention is to declare Exception as a class instance after the type, and for the type to be a Show instance for diagnostic output.
catch, handle, try
catch :: Exception e => IO a -> (e -> IO a) -> IO a
handle :: Exception e => (e -> IO a) -> IO a -> IO a -- = flip catch
try :: Exception e => IO a -> IO (Either e a)
The three forms admit different syntactic positions:
result <- catch action handler
result <- handle handler action
result <- try action :: IO (Either SomeException Result)
try is the most flexible — it produces a Maybe-style result. catch is the conventional form for inline handling.
The type-driven aspect of catching: the compiler determines which exception type to catch based on the handler’s signature:
result <- try action :: IO (Either IOException Result)
-- catches IOException only
For catching anything, use SomeException:
result <- try action :: IO (Either SomeException Result)
The SomeException is the supertype of all exceptions; it admits catching everything.
bracket for resource management
The conventional pattern for resource acquisition with guaranteed cleanup:
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
-- bracket acquire release action
main = bracket
(openFile "input.txt" ReadMode) -- acquire
hClose -- release
(\handle -> do -- action
contents <- hGetContents handle
process contents)
bracket runs the acquire, then the action, then the release — even if the action throws. The mechanism is the conventional Haskell idiom for resources that need explicit cleanup.
For one-off cleanup without a resource, finally:
finally :: IO a -> IO b -> IO a
main = do
setUp
doWork `finally` tearDown
finally runs the second action whether the first succeeds or fails.
For withFile, withSocketFD, and similar, the bracket-pattern is encapsulated:
import System.IO
main = withFile "input.txt" ReadMode $ \handle -> do
contents <- hGetContents handle
process contents
-- equivalent to: bracket (openFile ...) hClose (\h -> ...)
withFile is the conventional contemporary form for file operations.
Standard exception types
The base library defines several exception types:
| Type | Module | Use |
|---|---|---|
IOException | System.IO.Error | I/O errors (file not found, permission denied) |
ArithException | Control.Exception.Base | Division by zero, overflow |
ArrayException | Control.Exception.Base | Out-of-bounds array access |
ErrorCall | Control.Exception | The exception thrown by error |
PatternMatchFail | Control.Exception | Failure of a pattern match |
AsyncException | Control.Exception | Asynchronous interruption |
SomeException | Control.Exception | Supertype of all exceptions |
The IOException is by far the most common; it is thrown by file operations, network operations, and most other IO that may fail.
error and undefined
The error function throws an exception:
error :: String -> a
unsafeHead :: [a] -> a
unsafeHead [] = error "empty list"
unsafeHead (x:_) = x
undefined is the same as error "undefined":
undefined :: a
placeholder :: Int -> Int
placeholder = undefined -- to be implemented
Both are sometimes useful — for placeholders during development, for “this case should never happen” assertions — but the conventional discipline is to avoid them in production code. They produce runtime errors rather than compile-time errors and are difficult to recover from.
The compiler-friendly alternatives:
errorWithoutStackTrace—errorwithout a stack trace.Data.Maybe.fromMaybe,Data.Either.either— for total handling.- Type-driven exhaustive pattern matching — let the compiler verify that all cases are handled.
Pattern-match failure
When a function’s pattern match fails (no equation matches), the runtime throws PatternMatchFail:
unsafeHead :: [a] -> a
unsafeHead (x:_) = x
-- unsafeHead [] throws PatternMatchFail
The -Wincomplete-patterns (part of -Wall) warns about this at compile time. The conventional discipline is to enable the warning and handle every case.
MonadFail
MonadFail is the class for monads that admit pattern-match failure:
class Monad m => MonadFail m where
fail :: String -> m a
instance MonadFail Maybe where
fail _ = Nothing
instance MonadFail [] where
fail _ = []
instance MonadFail IO where
fail = throwIO . userError
In do-notation, a non-trivial pattern failure invokes fail:
processList :: [Int] -> Maybe Int
processList xs = do
Just n : _ <- pure (map (>= 0) xs) -- pattern match
return n
-- if the first element is False, fail is invoked, producing Nothing
The mechanism is occasionally surprising; the conventional discipline is to use explicit case for non-trivial patterns in do-notation.
The conventional discipline
The principal contemporary Haskell error-handling guidelines:
Prefer value-level error handling
-- For absence:
findUser :: UserId -> Maybe User
-- For value-or-error:
parseConfig :: String -> Either ConfigError Config
-- For accumulating errors:
validateForm :: Form -> Validation [Error] ValidForm
The mechanism is type-checked, exhaustive, and integrates with the rest of the language.
Use exceptions for IO and unexpected failures
-- File operations may throw IOException:
contents <- try (readFile "input.txt") :: IO (Either IOException String)
-- Network operations may throw various:
response <- try (http get "url") :: IO (Either HttpException Response)
The exception mechanism is appropriate for failures that are exceptional in the literal sense — failures the typical caller is not equipped to handle.
Use bracket for resource management
withDatabase $ \conn -> do
rows <- query conn "SELECT * FROM users"
process rows
The withResource pattern guarantees cleanup even on exception.
Avoid error and partial functions
-- Bad:
firstUser users = head users
-- Good:
firstUser users = listToMaybe users
Don’t catch SomeException indiscriminately
-- Bad: catches everything, including async exceptions
result <- try action :: IO (Either SomeException Result)
-- Good: catch the specific exception types
result <- try action :: IO (Either IOException Result)
Catching SomeException includes AsyncException (thread interruption), which the program almost never wants to handle. The conventional defence is to use specific exception types.
The safe-exceptions package provides a more principled wrapper that handles async exceptions correctly.
Common patterns
Validate-and-construct
data Email = Email String
mkEmail :: String -> Either String Email
mkEmail s
| '@' `elem` s = Right (Email s)
| otherwise = Left "must contain @"
The pattern admits invariant-preserving construction; consumers cannot construct an Email without going through the validator (assuming the constructor is not exported).
Chain fallible operations
processInput :: String -> Either String Result
processInput input = do
parsed <- parseInput input
valid <- validateInput parsed
enriched <- enrichWith parsed valid
return (computeResult enriched)
The Either e monad short-circuits on the first failure.
Try-catch-finally
import Control.Exception
main = do
result <- bracket
(openFile "input.txt" ReadMode)
hClose
(\h -> do
contents <- hGetContents h
return (process contents))
`catch` \(e :: IOException) -> do
putStrLn ("Error: " ++ show e)
return defaultResult
print result
The combination of bracket and catch admits resource management with exception handling.
Convert between Maybe/Either and exceptions
-- Maybe to exception:
fromJustOrThrow :: Exception e => e -> Maybe a -> IO a
fromJustOrThrow ex Nothing = throwIO ex
fromJustOrThrow _ (Just x) = return x
-- Either to exception:
fromEither :: Exception e => Either e a -> IO a
fromEither (Left e) = throwIO e
fromEither (Right x) = return x
The functions admit converting value-level errors to exceptions when the calling context requires the exception form.
Maybe to default
import Data.Maybe (fromMaybe)
displayName user = fromMaybe "stranger" (userName user)
fromMaybe admits “use this default if absent”.
Either to logged failure
import Data.Either (either)
logResult :: Either Error Success -> IO ()
logResult = either (logError . show) (logSuccess . show)
The either function admits handling both cases without an explicit case.
A note on the broader landscape
The Haskell community has explored several alternatives to the conventional Maybe/Either/exception story:
Validation(validationpackage) — applicative validation that accumulates errors.These(thesepackage) — three-way “left or right or both” type.MonadError(mtlpackage) — class abstraction overEither-like effects.MonadThrow/MonadCatch(exceptionspackage) — class abstractions over exception-like effects.safe-exceptions— a more principled wrapper that distinguishes synchronous from asynchronous exceptions.- Effect systems (
polysemy,effectful) — first-class error effects with explicit handlers.
For most application code, Maybe, Either, and the standard exception machinery suffice; the more elaborate libraries are appropriate for codebases that need richer error semantics. The conventional contemporary advice is to start simple and adopt the more elaborate machinery only when the simpler tools become inadequate.