← Fundamentals
Guided lesson 6 questions · 6 languages

Errors as values or exceptions

Return codes, error values, Result, and exceptions: how a failure reaches the code that asked.

A failure reaches the caller in one of two ways: as a returned value that the caller must look at, or as a jump up the call stack that skips code until something catches it. Each language has a usual way, and each way can hide a failure in its own place.

Before you begin

Why this matters

Maybe and Either put a possible failure into the type, so the compiler makes code handle it. The failures that slip past are the ones the type does not show, such as head on an empty list.

What this helps you reason about

  • Follow a Left from the function that makes it to the code that prints it.
  • Spot head and fromJust, which can fail although their types do not say so.
  • Turn a Maybe into an Either and back, and know what each loses.

A first-aid kit holds 3 plasters, and two patients need 2 each. The second take must fail. In Haskell, a failure that a caller should expect is a value: Maybe when there is no reason to give, and Either when there is. Some functions, such as head, can still fail with an exception.

Already know this? The short version
  • Haskell passes expected failures as values: Maybe a (Nothing or Just x) when no reason is needed, and Either e a (Left reason or Right x) when it is.
  • A Left travels only through code that passes it on, with a case expression or a do block in Either, and the type of every function on the way shows it.
  • Exceptions exist too: head [], fromJust Nothing, and error "…" raise one when their result is demanded, and only IO code can catch it, with Control.Exception.
  • maybe, either, and fromMaybe turn a failure into an ordinary value; a Maybe becomes an Either when you add a reason.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
Either
A type whose values are Left e or Right x. By convention Left holds an error and Right holds a successful result.
Maybe
A type whose values are Just x, holding a value, or Nothing, holding none. Maybe Int, for example, is a number that may be missing.
case expression
An expression written case value of, followed by alternatives of the form pattern -> result. It gives the result of the first alternative whose pattern matches.
propagate
To pass a failure on to your own caller instead of handling it. An exception propagates by itself; a failure returned as a value propagates only when each caller returns it again.
exception
An error raised while a program runs, such as the one !! raises for a position past the end. Unless the program handles it, the program stops and prints its message.
partial function
A function that has no result for some inputs of its type, such as head on an empty list, or !! with a position past the end. The type checker accepts every call; the failure appears only when the missing result is needed.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Two ways

How can a failure reach the caller?

A function that cannot give its result must tell its caller. Languages use two ways.

In the first way, the failure is a value. The function returns in the normal way, and what it returns says that it failed. The caller must look at that value to notice the failure.

In the second way, the failure is an exception. The function does not return in the normal way. The failure jumps up the call stack: the frame of the failing function ends, then its caller’s frame ends, and so on, until the failure reaches code that catches it. The lines in between are skipped.

Haskell uses values. Either String Int is a type whose values are Left with a String, the reason, or Right with an Int, the result. takePlasters gives one or the other, and a case expression looks at which one it got.

Main.hs
takePlasters :: Int -> Int -> Either String Int
takePlasters left wanted
  | wanted > left =
      Left ("need " ++ show wanted ++ ", have " ++ show left)
  | otherwise = Right (left - wanted)

main :: IO ()
main =
  case takePlasters 3 4 of
    Left problem -> putStrLn ("failed: " ++ problem)
    Right rest -> print rest

A failure reaches the caller either as a value it must look at, or as an exception that jumps up the call stack. Haskell uses values for failures a caller should expect.

Coming from Java: There is no throw for an expected failure: takePlasters gives a Left, an ordinary value, and the type Either String Int says that it can.

Experiment

Follow a Left to the code that prints it

takePlasters gives a Left, an ordinary value. treat’s case passes it on unchanged, and main’s case decides what to do with it. Nothing jumps; the Left is a result like any other.

Why it matters A Left reaches main only because treat passes it on. The type Either String Int on treat shows that it can.

Predict what is printed, and which takePlasters gives the Left. Then change treat 3 to treat 4 and predict again.

takePlasters :: Int -> Int -> Either String Int
takePlasters left wanted
  | wanted > left =
      Left ("need " ++ show wanted ++ ", have " ++ show left)
  | otherwise = Right (left - wanted)

treat :: Int -> Either String Int
treat left =
  case takePlasters left 2 of
    Left problem -> Left problem
    Right afterFirst -> takePlasters afterFirst 2

main :: IO ()
main =
  case treat 3 of
    Left problem -> putStrLn ("failed: " ++ problem)
    Right rest -> putStrLn ("left: " ++ show rest)
What to look for

GHCi shows one location per stop, not a stack of frames, and it stops where a value is demanded, not in the order of the lines. main demands treat 3 first, so the stepper stops in treat, then in takePlasters with left 3. afterFirst shows _ (unevaluated) until the second takePlasters needs it. main’s case takes the Left alternative, and the output grows piece by piece as the message is demanded: first “failed: ”, then “failed: need ”, then the numbers. That is demand, not a delay in printing. The run prints failed: need 2, have 1. After the edit, treat 4 gives Right 0, and the run prints left: 0.

Expected output

failed: need 2, have 1

02 / Values

What does a failure look like as a value?

Maybe Int says that a result may be missing, as in Absence, zero, and empty values. Either String Int also says why. A Left reaches the caller only through code that passes it on: treat below gives back the Left from its first takePlasters unchanged. That is propagating the failure. Its type, Either String Int, shows that it can fail too.

Main.hs
treat :: Int -> Either String Int
treat left =
  case takePlasters left 2 of
    Left problem -> Left problem
    Right afterFirst -> takePlasters afterFirst 2

A Left reaches the caller only through code that passes it on, and every type on the way shows it.

Precisely: do blocks in Either

A do block also works for Either: do { afterFirst <- takePlasters left 2; takePlasters afterFirst 2 } passes the first Left on by itself, much like Rust’s ?. A later lesson, Haskell Maybe, Either, and partial functions, explains why.

03 / Exceptions

What happens when a failure jumps up the call stack?

Haskell has exceptions too. head on an empty list, fromJust Nothing, and error "…" have no result to give, so they raise an exception when their result is demanded. Unless code catches it, the program stops with a message such as *** Exception: Prelude.head: empty list. Only an IO action can catch it, with catch or try from Control.Exception; a pure function cannot.

head is a partial function: its type, [a] -> a, promises a result that it cannot always give.

Main.hs
firstFull :: [Int] -> Int
firstFull boxes = head (filter (> 0) boxes)
-- firstFull [0, 0] raises Prelude.head: empty list

Haskell’s exceptions come from partial functions such as head; only IO code can catch them.

04 / Costs

Where can each way hide a failure?

Each way has a cost. Maybe and Either show in the type, and code cannot use the Int inside a Right without first dealing with the Left. So a failure is hard to drop by accident. It can still be dropped on purpose: fromMaybe 0, or either (const 0) id, turns it into a 0 that looks real.

The failures that the type does not show come from partial functions. head (filter (> 0) boxes) has the type Int, so nothing reminds the caller that there may be no box. Match on the empty list instead, and give back a Maybe.

Main.hs
firstFull :: [Int] -> Maybe Int
firstFull boxes =
  case filter (> 0) boxes of
    [] -> Nothing
    first : _ -> Just first

Maybe and Either show in the type; head and fromJust hide a failure behind a type that promises a value.

Coming from Rust: head and fromJust are Haskell’s unwrap: a crash hidden behind a type that promises a value.

Experiment

Take the head of a list that can be empty

filter (> 0) keeps the boxes that hold plasters. With [0, 0, 0] it gives [], and head has no first element to give, so it raises an exception when its result is demanded. The type of firstFull, [Int] -> Int, promised an Int.

Why it matters A type that says Int cannot say “or no value”. Where a list can be empty, match on [] or give back a Maybe Int.

Predict what is printed. Then change [0, 0, 5] to [0, 0, 0] and predict again.

-- Plasters in each box on the shelf, in order.
firstFull :: [Int] -> Int
firstFull boxes = head (filter (> 0) boxes)

main :: IO ()
main = do
  let shelf = [0, 0, 5]
  putStrLn ("open a box of " ++ show (firstFull shelf))
What to look for

GHCi stops at the putStrLn line first, and the output already shows “open a box of” before firstFull runs: the text is demanded piece by piece. Then it stops in firstFull, where boxes shows _ (unevaluated), and in shelf. The run prints open a box of 5. After the edit, the run stops with an error, *** Exception: Prelude.head: empty list, and the output keeps “open a box of”.

Expected output

open a box of 5

05 / Converting

How do you turn one kind of failure into the other?

Converting between failures uses ordinary functions. maybe d f m gives d for Nothing and f x for Just x; either f g e does the same for Left and Right. A Maybe becomes an Either when you add a reason, and an Either becomes a Maybe when you drop it.

An exception becomes a value only in IO: try (evaluate x) from Control.Exception gives Left with the exception, or Right with x.

Main.hs
toEither :: Maybe Int -> Either String Int
toEither = maybe (Left "no full box") Right

toMaybe :: Either String Int -> Maybe Int
toMaybe = either (const Nothing) Just

maybe and either convert between Maybe, Either, and plain values; only IO turns an exception into a value.

Precisely: catching head’s exception

In GHC 9.0, head [] raises an ErrorCall. try (evaluate (firstFull [])) :: IO (Either ErrorCall Int) gives Left with it. evaluate is needed because try catches only what happens while the IO action runs, and a lazy value is computed later.

06 / Try it

Can you explain a fresh example?

Predict every line of output, then compare with the answer. The next lesson, Stack and heap, shows where values are stored while a program runs. Its link is at the end of the page.

Main.hs
takePlasters :: Int -> Int -> Either String Int
takePlasters left wanted
  | wanted > left =
      Left ("need " ++ show wanted ++ ", have " ++ show left)
  | otherwise = Right (left - wanted)

treat :: Int -> Either String Int
treat left =
  case takePlasters left 2 of
    Left problem -> Left problem
    Right afterFirst -> takePlasters afterFirst 2

main :: IO ()
main = do
  print (treat 3)
  print (treat 5)

What is printed, line by line?

Reveal the reasoning

Left "need 2, have 1", then Right 1. With 3 plasters, the first takePlasters gives Right 1, and the second needs 2 of the 1 left, so it gives a Left, which treat gives back as its result. With 5, both succeed: 5 - 2 - 2 is 1. print shows the alternative, and a String in quotes.

Practice tickets

  1. ERR-101 · An unreadable weight gives a dose of 0 mg → Free
  2. ERR-102 · One unreadable drink hides the rest of the fluid chart → Sign in · Pro
  3. ERR-103 · Medicine packs vanish when a ward cabinet is full → Sign in · Pro

Keep exploring

Stack and heap →

Primary references