7 min read
Error Handling
Prerequisites: Maybe and Either
Haskell has two kinds of error handling, and good code keeps them separate:
- Expected failures in pure code — invalid input, a missing key.
Represent them in the type with
MaybeorEither. - Exceptions in IO — a missing file, a network failure. These are
thrown and caught with
Control.Exception.
Pure errors: Either all the way
data OrderError = EmptyCart | InvalidQuantity String Int | UnknownItem String
deriving (Show)
prices :: [(String, Double)]
prices = [("tea", 3.5), ("cake", 4.0)]
lineTotal :: (String, Int) -> Either OrderError Double
lineTotal (item, qty)
| qty <= 0 = Left (InvalidQuantity item qty)
| otherwise = case lookup item prices of
Nothing -> Left (UnknownItem item)
Just p -> Right (p * fromIntegral qty)
orderTotal :: [(String, Int)] -> Either OrderError Double
orderTotal [] = Left EmptyCart
orderTotal items = sum <$> traverse lineTotal items
main :: IO ()
main = do
print (orderTotal [("tea", 2), ("cake", 1)])
print (orderTotal [("tea", 2), ("coffee", 1)])
print (orderTotal [("tea", 0)])
print (orderTotal [])
Right 11.0
Left (UnknownItem "coffee")
Left (InvalidQuantity "tea" 0)
Left EmptyCart
traverse runs lineTotal on every item and stops at the first Left.
Exceptions in IO
import Control.Exception
import System.IO.Error (isDoesNotExistError)
main :: IO ()
main = do
result <- try (readFile "missing.txt") :: IO (Either IOException String)
case result of
Left e -> putStrLn ("Could not read file: " ++ (if isDoesNotExistError e then "it does not exist" else show e))
Right text -> putStrLn text
r <- try (evaluate (1 `div` (0 :: Int)))
case r of
Left DivideByZero -> putStrLn "caught division by zero"
Left e -> putStrLn ("other arithmetic error: " ++ show e)
Right v -> print v
handle (\(ErrorCall msg) -> putStrLn ("caught error: " ++ msg)) $ do
_ <- evaluate (length [1, 2, error "never evaluated"] + error "boom")
return ()
Could not read file: it does not exist
caught division by zero
caught error: boom
try actionreturnsLeft exceptionorRight result. The type annotation chooses which exception type to catch.evaluateforces a pure value insideIO, so an exception from it is raised there — laziness otherwise delays errors until the value is used, possibly outside your handler.handle(andcatch, with arguments the other way round) runs a handler if the action throws.
Throwing your own exceptions
import Control.Exception
data ConfigError = MissingKey String
deriving (Show)
instance Exception ConfigError
getSetting :: String -> [(String, String)] -> IO String
getSetting key cfg = case lookup key cfg of
Just v -> return v
Nothing -> throwIO (MissingKey key)
main :: IO ()
main = do
let cfg = [("host", "localhost")]
getSetting "host" cfg >>= putStrLn
r <- try (getSetting "port" cfg)
case r of
Left (MissingKey k) -> putStrLn ("missing setting: " ++ k)
Right v -> putStrLn v
(putStrLn "working..." >> throwIO (MissingKey "db")) `catch` \e ->
putStrLn ("handled " ++ show (e :: ConfigError))
putStrLn "done" `finally` putStrLn "cleanup always runs"
localhost
missing setting: port
working...
handled MissingKey "db"
done
cleanup always runs
Guidelines
- Use
Maybe/Eitherfor failures callers are expected to handle. - Use
throwIOin IO code for truly exceptional situations, and catch them near the top of the program. - Avoid
errorand partial functions (head,fromJust,read) in library code: they crash with little context. - Use
bracketorfinally(orwithFile) to release resources even when an exception occurs.