Maybe and Either for Explicit Failure
Haskell represents optional or absent values with Maybe a (Nothing or Just a), and failures that need an explanation with Either e a (Left e for failure with a reason, Right a for success). Because both possibilities are encoded directly in the function's type signature, the compiler forces every caller to pattern-match or otherwise handle the failure case -- unlike exceptions, which are completely invisible in a type signature like readFile :: FilePath -> IO String.
Cricket analogy: A function returning Maybe Int instead of just Int is like a scorecard that explicitly has a 'did not bat' entry rather than silently listing a 0, forcing anyone reading it to distinguish 'scored zero' from 'never got the chance,' exactly the distinction between Just 0 and Nothing.
Chaining Failure with Monads
Because Maybe and Either e are both monads, you can chain a sequence of steps that might fail using do-notation or >>=, and the chain automatically short-circuits at the first Nothing or Left, skipping every remaining step. For application code that needs to combine this kind of explicit, short-circuiting error handling with IO, the ExceptT e IO a monad transformer (and MaybeT for the Maybe case) layers Either-style failure on top of another monad so you can fail explicitly while still performing IO.
Cricket analogy: Chaining Either String computations with do-notation so the first Left short-circuits the rest is like a bowling review process where if the front-foot no-ball check fails first, the umpire doesn't even bother checking the LBW angle afterward.
import Control.Exception (catch, SomeException, try)
data ValidationError = EmptyName | InvalidAge Int
deriving Show
validateUser :: String -> Int -> Either ValidationError (String, Int)
validateUser name age = do
n <- if null name then Left EmptyName else Right name
a <- if age < 0 || age > 130 then Left (InvalidAge age) else Right age
Right (n, a)
main :: IO ()
main = do
print (validateUser "Ada" 36) -- Right ("Ada",36)
print (validateUser "" 36) -- Left EmptyName
result <- try (readFile "missing.txt") :: IO (Either SomeException String)
case result of
Left err -> putStrLn ("Failed to read file: " ++ show err)
Right content -> putStrLn content
Runtime Exceptions: throw, catch, and SomeException
Control.Exception provides throwIO, catch, and try for genuine runtime and IO-level failures like a missing file or a network timeout, built on an extensible Exception typeclass so any exception type can be caught generically as SomeException or specifically by its concrete type. Exceptions are best reserved for truly exceptional, IO-boundary conditions -- not for routine control flow like validating a form field, which is exactly what Either was designed for.
Cricket analogy: catch handling a SomeException thrown by a truly unexpected failure is like a ground's contingency plan for a power outage during a floodlit match -- a rare, exceptional event handled with a special recovery procedure, not part of the normal over-by-over flow.
Partial functions like head, tail, and fromJust throw an unchecked, pure runtime exception (Prelude.head: empty list) with no trace in the type signature -- head :: [a] -> a looks total but isn't, so a caller has no compiler-enforced hint they need to handle the empty-list case; prefer Data.List.NonEmpty or pattern matching with an explicit Maybe return instead.
Choosing Between Maybe, Either, and Exceptions
A practical rule of thumb: use Maybe for simple, self-explanatory absence where no extra context is needed (does this list have a head?); use Either when a caller genuinely needs to know *why* something failed, especially for expected business-rule failures like validation errors; and reserve real exceptions for IO-boundary conditions that are truly unexpected and where recovery, if any, happens at a much higher level -- disk failures, network timeouts, or corrupted external data.
Cricket analogy: Choosing Maybe for a simple lookup like 'did this batter score a century' versus Either with a detailed reason for 'why was this delivery called a no-ball' versus a true exception only for something like the stadium's power failing mid-over mirrors matching the right tool to how much explanation the failure deserves.
Control.Exception.bracket acquire release use guarantees release runs even if use throws an exception, making it the standard pattern for exception-safe resource management in Haskell -- for example, bracket (openFile p ReadMode) hClose $ \h -> ... always closes the handle, whether or not reading throws.
Maybe a(Nothing/Just) represents simple absence;Either e a(Left e/Right a) represents failure carrying an explanatory error value.- Both encode failure in the type signature, forcing callers to handle it -- unlike exceptions, which are invisible to the type checker.
- Do-notation and
>>=short-circuit a chain of Maybe/Either computations on the first Nothing/Left, skipping the remaining steps. ExceptT/MaybeTmonad transformers let you combine Maybe/Either-style short-circuiting with IO or other monads.- Control.Exception provides
throwIO,catch, andtryfor genuine runtime/IO-level failures via the extensibleExceptiontypeclass andSomeException. - Partial functions like
headandfromJustthrow untyped runtime exceptions that bypass the compiler's guarantees -- avoid them in production code paths. bracketguarantees resource cleanup even when an exception is thrown, and is the standard pattern for exception-safe resource handling.
Practice what you learned
1. What is the key difference between `Maybe a` and `Either e a` for representing failure?
2. What happens when a do-block chaining `Either String a` computations hits a `Left` value partway through?
3. Why is `head :: [a] -> a` considered a 'partial function' and a common source of bugs?
4. What guarantee does `Control.Exception.bracket acquire release use` provide?
5. Which failure scenario is most appropriate to represent with a genuine runtime exception rather than Either or Maybe?
Was this page helpful?
You May Also Like
Lazy Evaluation in Haskell
How Haskell's non-strict evaluation model defers computation until results are needed, enabling infinite structures but requiring care around space leaks.
Testing Haskell Code
Learn how to write unit tests, property-based tests, and doctest examples for Haskell code using HUnit, Hspec, and QuickCheck.
Concurrency in Haskell
Learn how Haskell's lightweight threads, MVar, STM, and the async library make concurrent and parallel programming safer and more composable.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics