Error handling in Haskell differs significantly from exception-based approaches in imperative languages. Haskell provides several approaches that align with its functional nature and type system.
Types of Errors in Haskell
- Compile-time errors: Type errors, syntax errors, etc.
- Runtime errors: Exceptions that occur during program execution
- Expected failures: Situations where operations might legitimately fail
This page focuses on handling the last two categories.
Approaches to Error Handling
1. Maybe Type
The Maybe type represents computations that might fail:
data Maybe a = Nothing | Just aUse Cases
- Partial functions (not defined for all inputs)
- Functions that could fail without needing detailed error information
Example
safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide x y = Just (x `div` y)
-- Usage
case safeDivide 10 0 of
Nothing -> putStrLn "Division by zero!"
Just result -> putStrLn $ "Result: " ++ show resultWorking with Multiple Maybe Values
-- Using do notation
computeAverage :: [Int] -> Maybe Double
computeAverage xs = do
if null xs
then Nothing
else Just (fromIntegral (sum xs) / fromIntegral (length xs))
-- Using monadic functions
findUserAvgScore :: UserId -> [ScoreId] -> Maybe Double
findUserAvgScore uid scoreIds = do
user <- findUser uid
scores <- mapM (getScore user) scoreIds
return (average scores)2. Either Type
The Either type provides more detailed error information:
data Either a b = Left a | Right bBy convention, Left contains error information and Right contains successful results.
Use Cases
- When you need specific error information
- Validation that needs to report why it failed
- Functions with multiple failure modes
Example
data DivideError = DivideByZero | OverflowError
safeDivide :: Int -> Int -> Either DivideError Int
safeDivide _ 0 = Left DivideByZero
safeDivide x y
| x > maxBound `div` y = Left OverflowError
| otherwise = Right (x `div` y)
-- Usage
case safeDivide 10 0 of
Left DivideByZero -> putStrLn "Cannot divide by zero!"
Left OverflowError -> putStrLn "Overflow would occur!"
Right result -> putStrLn $ "Result: " ++ show resultWorking with Multiple Either Values
-- Using do notation
validatePerson :: String -> Int -> Either String Person
validatePerson name age = do
validName <- validateName name
validAge <- validateAge age
Right (Person validName validAge)
where
validateName "" = Left "Name cannot be empty"
validateName n = Right n
validateAge a
| a < 0 = Left "Age cannot be negative"
| a > 120 = Left "Age is unrealistic"
| otherwise = Right a3. ExceptT Monad Transformer
ExceptT combines the Either type with another monad (often IO):
newtype ExceptT e m a = ExceptT { runExceptT :: m (Either e a) }Use Cases
- Handling errors in code that also performs IO or other monadic operations
- Creating a clean separation of error handling from other effects
- Building complex functions that can fail at multiple points
Example
import Control.Monad.Except
type AppError = String
type App a = ExceptT AppError IO a
readConfig :: FilePath -> App Config
readConfig path = do
exists <- liftIO $ doesFileExist path
unless exists $
throwError $ "Config file not found: " ++ path
content <- liftIO $ readFile path
case parseConfig content of
Nothing -> throwError "Invalid config format"
Just config -> return config
runApp :: App a -> IO (Either AppError a)
runApp = runExceptT4. Runtime Exceptions
Haskell has a system for runtime exceptions, but it’s generally avoided in favor of explicit error types:
-- Generating exceptions
error :: String -> a
undefined :: a
-- Catching exceptions
catch :: Exception e => IO a -> (e -> IO a) -> IO a
try :: Exception e => IO a -> IO (Either e a)Example
import Control.Exception
-- Catching exceptions
readFileContent :: FilePath -> IO String
readFileContent path = readFile path `catch` handleError
where
handleError :: IOException -> IO String
handleError e = return $ "Error reading file: " ++ show e
-- Using try
readFileContent' :: FilePath -> IO (Either IOException String)
readFileContent' path = try (readFile path)5. Custom Exceptions
You can define custom exception types:
import Control.Exception
data AppException = ConfigError String
| DatabaseError String
| NetworkError String
deriving (Show, Typeable)
instance Exception AppException
-- Throwing custom exceptions
throwConfigError :: String -> IO a
throwConfigError msg = throwIO (ConfigError msg)
-- Catching specific exception types
catchAppExceptions :: IO a -> IO a
catchAppExceptions action = action `catches`
[ Handler (\(e :: ConfigError) -> handleConfigError e)
, Handler (\(e :: DatabaseError) -> handleDatabaseError e)
, Handler (\(e :: SomeException) -> handleOtherExceptions e)
]Validation: Collecting Multiple Errors
Sometimes you want to report all errors rather than just the first one:
import Data.Validation
data ValidationError = NameTooShort
| AgeTooLow
| InvalidEmail
deriving (Show)
validatePerson :: String -> Int -> String -> Validation [ValidationError] Person
validatePerson name age email =
Person <$> validateName name <*> validateAge age <*> validateEmail email
where
validateName n
| length n < 2 = Failure [NameTooShort]
| otherwise = Success n
validateAge a
| a < 18 = Failure [AgeTooLow]
| otherwise = Success a
validateEmail e
| not (isValidEmail e) = Failure [InvalidEmail]
| otherwise = Success eError Handling Patterns
Defensive Programming
processData :: Maybe Data -> Either Error Result
processData Nothing = Left (MissingData "No data provided")
processData (Just d)
| isValid d = Right (computeResult d)
| otherwise = Left (InvalidData "Data is invalid")Railway-Oriented Programming
Thinking of functions as “tracks” where success follows one track and failure another:
validateInput :: Input -> Either Error ValidInput
transformData :: ValidInput -> Either Error TransformedData
computeOutput :: TransformedData -> Either Error Output
processInput :: Input -> Either Error Output
processInput input = do
validInput <- validateInput input
transformedData <- transformData validInput
computeOutput transformedDataError Handling with Monads
Using monadic functions for cleaner error handling:
-- mapM for processing lists with potential failures
processItems :: [Item] -> Either Error [Result]
processItems = mapM processItem
-- filterM for filtering with side effects
getValidItems :: [Item] -> IO [Item]
getValidItems = filterM isValid
-- Using forM_ for actions that might fail
processAllItems :: [Item] -> Either Error ()
processAllItems items = forM_ items $ \item -> do
result <- processItem item
storeResult resultBest Practices
-
Use the simplest mechanism that meets your needs:
- For simple absence/presence of a value, use
Maybe - For error details, use
Either - For more complex requirements, consider monad transformers
- For simple absence/presence of a value, use
-
Make error types informative:
data UserError = UserNotFound UserId | InvalidPassword | AccountLocked DateTime -
Return errors, don’t throw exceptions in pure code:
-- Good lookup :: Key -> Map Key Value -> Maybe Value -- Avoid lookup :: Key -> Map Key Value -> Value lookup k m = case Map.lookup k m of Just v -> v Nothing -> error "Key not found" -
Handle all cases explicitly:
processResult :: Either Error Value -> String processResult (Right value) = "Success: " ++ show value processResult (Left (InputError msg)) = "Input error: " ++ msg processResult (Left (SystemError code)) = "System error: " ++ show code processResult (Left (OtherError e)) = "Other error: " ++ show e -
Compose error-handling functions using monads:
validateAndProcess :: Input -> Either Error Output validateAndProcess = validate >=> process >=> format
Key Points to Remember
- Haskell favors explicit error handling using types like
MaybeandEither - Exceptions in Haskell are primarily for exceptional conditions, not regular error handling
- Monadic composition makes it easy to chain operations that might fail
- Custom error types help make error handling more informative and type-safe
- For IO operations, consider using
ExceptTto combine IO with structured error handling