Error Handling and Exceptions

23.1 Pure Errors: Maybe, Either, and Friends

Use Maybe when there's one obvious way to fail, Either e when callers need to know why, and ExceptT (Module 26) to combine Either with other effects.

data AppError = NotFound String | InvalidInput String | Unauthorized
  deriving (Show, Eq)

findUser :: Int -> Either AppError String
findUser 1 = Right "ana"
findUser n = Left (NotFound ("user " ++ show n))

23.2 Exceptions in IO

throwIO / throw Raise (prefer throwIO in IO; throw from pure code is imprecise) try, catch, handle, catches

bracket, bracket_, finally, onException Force a pure value inside IO so its exceptions are caught there

error, undefined, errorWithoutStackTrace Programmer errors (crash with a message) throwTo, killThread

Asynchronous exceptions (timeouts, killThread) can arrive at any point. Use bracket/mask for safety, and don't catch SomeException carelessly (it also catches async exceptions). Libraries like safe-exceptions/unliftio provide safer defaults.

HasCallStack constraints add stack traces to error: head' :: HasCallStack => [a] -> a.

import Control.Exception

data BankError = InsufficientFunds Double | AccountLocked
  deriving Show
instance Exception BankError              -- custom exception type

withdraw :: Double -> Double -> IO Double
withdraw balance amount
  | amount > balance = throwIO (InsufficientFunds (amount - balance))
  | otherwise = pure (balance - amount)

main :: IO ()
main = do
  r <- try (withdraw 100 150) :: IO (Either BankError Double)
  case r of
    Left (InsufficientFunds short) -> putStrLn ("Short by " ++ show short)
    Left e -> putStrLn ("Other error: " ++ show e)
    Right b -> putStrLn ("New balance: " ++ show b)

  -- catch with a handler
  readFile "missing.txt" `catch` \(e :: IOException) -> do   -- ScopedTypeVariables (GHC2021)
    putStrLn ("IO error: " ++ show e)
    pure ""

  -- handle multiple exception types
  result <- (evaluate (1 `div` 0 :: Int) >> pure "ok") `catches`
    [ Handler (\(e :: ArithException) -> pure ("arith: " ++ show e))
    , Handler (\(e :: SomeException) -> pure ("other: " ++ displayException e))
    ]
  putStrLn result

  -- cleanup
  (putStrLn "working" >> throwIO AccountLocked) `catch` (\(e :: BankError) -> print e)
    `finally` putStrLn "always 
…