Exceptions
A function's type tells you the shape of its output: int -> int
returns an int, 'a list -> int returns the length of a list,
and so on. But what if the function is asked to compute something
that has no answer? List.hd [] cannot return an int: the empty
list has no head. 1 / 0 cannot return an int: integer division
by zero is undefined. int_of_string "hello" cannot return an
int: the string is not a number.
We have already seen one way to express this: the
option type
(None when there is no answer, Some x when there is) and the
result type
(Error e instead of just None, so the failure can carry a
reason). Those were the subjects of
the recursive-types lecture.
They are the typed
approach to partial functions: the possibility of failure is right
there in the return type, and the compiler will not let you forget
to handle it.
This lecture covers the other approach: exceptions. An exception
is a kind of value that, when raised, interrupts the normal flow
of evaluation and propagates up the call stack until something
catches it. Exceptions are how OCaml expresses "something went
wrong in a way the caller probably is not going to handle here,
but somebody up the call stack might want to." They are cheap at
the call site (no wrapping in Some, no unwrapping with match)
but they hide the possibility of failure from the type. We will
spend the lecture on what exceptions are, how to raise and catch
them, and the practical question of when an exception is the
right shape and when an option or result is.
What is an exception?
An exception in OCaml is a kind of value, much like a
variant constructor. It has its own
type, exn. You declare a new exception with the exception
keyword, using exactly the syntax of a variant constructor:
Negative_input is a nullary exception constructor, like None
in option. Bad_index 7 is a payload-carrying value, like
Some 7. The standard library predefines a handful of these
(Failure, Not_found, Division_by_zero, ...); you can
declare your own.
What distinguishes an exception value from any other value is what you do with it. The language offers two primitives:
raise EXN_VALsignals that the exception just happened. Evaluation of the surrounding expression stops; the exception propagates up the call stack until something catches it. If nothing catches it, the program halts and prints the exception's name (and payload) to standard error.try EXPR with PATTERN -> HANDLERrunsEXPR. IfEXPRreturns a value, that value is the result of the wholetry-expression. IfEXPRraises an exception that matchesPATTERN,HANDLERis evaluated instead and its result is the result of the wholetry. Thewithclauses are pattern matches against exception values.
The next two sections work through each primitive in turn.
Raising exceptions
To raise an exception, apply the primitive raise to an
exception value:
The raise Negative_input branch never returns;
control jumps out of factorial and up the call stack until
something catches the exception (we will get to catching in the
next section). If nothing does, the program halts and prints the
exception.
The odd type of raise
The standard library gives raise the type:
val raise : exn -> 'a
This is an unusual signature. Most OCaml functions have a
specific result type: int_of_string : string -> int,
List.length : 'a list -> int. raise's result type is 'a, a
type variable unconstrained by its argument. Read the signature
literally: it claims to take an exn and produce a value of
any type the caller asks for.
The reason this is sound is that raise never actually returns
a value. It transfers control to the nearest handler. The
result type can be anything because no result will ever flow
back through it. The type checker uses that latitude exactly
once: at the call site. Wherever the raise sits in a program,
the surrounding context expects some type T, and 'a is
unified with T. That is what lets raise Negative_input sit
on one branch of if n < 0 then ... else ... opposite an int
branch: the int constrains 'a to int, and the types match.
Without this polymorphism, the language would need a separate
raise_int, raise_string, raise_bool, and so on.
Two convenience wrappers: failwith and invalid_arg
The standard library defines two convenience wrappers for the most common case, where the exception you want to raise is just "something went wrong, here's a message":
failwith sis exactlyraise (Failure s).invalid_arg sis exactlyraise (Invalid_argument s).
These are not new primitives, just shorthand for raise applied
to one of the two stdlib-predefined exception constructors. So a
function that fails on the empty list can be written:
The failwith arm expands to
raise (Failure "head of empty list"). The expanded form makes
the construction explicit: an exception value is a constructor
applied to its payload, exactly like Some 3 is Some applied
to 3.
Catching exceptions: try ... with
try ... with is an expression, like
if and
match. It produces a value, and
that value can be used wherever a value of its type is expected.
The general shape is:
try
EXPR
with
| PATTERN_1 -> HANDLER_1
| PATTERN_2 -> HANDLER_2
| ...
| PATTERN_N -> HANDLER_N
Evaluation order:
- Evaluate
EXPR. If it returns a value, that value is the result of the wholetry. The handlers are not visited. - If
EXPRraises an exception, the exception value is matched againstPATTERN_1,PATTERN_2, ... in order. The first matching clause'sHANDLER_iis evaluated and its result is the result of the wholetry. - If no
PATTERN_imatches, the exception keeps propagating up the call stack. Thetrydoes not consume it.
The with clauses are real
patterns: they match on the
exception constructor, can bind the payload, and can use
or-patterns
and the wildcard _.
The type rule
Because try ... with is one expression, OCaml needs to give
the whole thing a single type. The rule is what you would
expect by analogy with if and match:
EXPRhas some typeT.- Every
HANDLER_imust also have typeT.
Then the whole try expression has type T. If any handler
returns a different type, the compiler rejects the whole try.
A first example
List.hd raises Failure "hd" on the empty list; the with
clause catches Failure _ (the wildcard _ ignores the
message) and produces None. Both Some (List.hd xs) and
None have type int option, so the whole try has type
int option.
Multiple exception patterns
The with clause can list several patterns separated by |,
one per exception you want to handle:
Each
clause has the same type as Ok (f x), namely
(int, string) result, so the whole try is well-typed at that
type. Exceptions not listed (e.g., Stack_overflow) keep
propagating up; the try does not silently swallow them.
A wildcard | _ -> ... at the end would catch every exception,
but this is almost always a mistake: it hides bugs that would
otherwise surface as a crash. Catch specific exceptions; let
unexpected ones propagate.
Nested try ... with
A try is just an expression, so you can nest one inside the
body of another. The general rule is unchanged: when an exception
is raised, it is matched against the nearest enclosing try's
patterns first; only if that try does not match does the
exception keep propagating outward.
parse_and_divide "10" "2"returns5: both parses succeed,10 / 2 = 5.parse_and_divide "oops" "2"returns0: parsingsaraisesFailure "int_of_string", the innertryis the nearest enclosing one, it matchesFailure _and substitutes0fora. The outertrynever gets a chance.parse_and_divide "10" "oops"returns-1: parsingsasucceeds, so the innertryfinishes normally. ParsingsbraisesFailure, but now the innertryis no longer on the call stack. The exception propagates to the outertry, which matchesFailure _and returns-1.parse_and_divide "10" "0"returns-2: both parses succeed, but10 / 0raisesDivision_by_zero. The innertryis long gone; the outer catches and returns-2.
The mental model is the same as with try and an unmatched
clause: an exception walks up the call stack, visiting each
try it meets in turn, taking the first matching clause it
finds. Nesting just makes the "nearest" relationship explicit.
Built-in exceptions
The standard library predefines a handful of exception
constructors that show up routinely in OCaml code. Now that you
know raise and try ... with, here is the tour:
Failure of stringis raised byfailwith "...". It signals "the function was called in a way the documentation forbids."Invalid_argument of stringis raised byinvalid_arg "...". Used for outright invalid inputs:String.get s iwithiout of range, for instance.Not_foundis raised by lookup functions when the key is absent.List.assoc,Hashtbl.find, and many others raise it.Division_by_zerois raised by/andmodon integer zero.End_of_fileis raised by reading-from-channel functions when they hit the end of input.Assert_failure of (string * int * int)is raised by the built-inassertkeyword when its condition is false. The payload is(file, line, column)of the failing assertion, filled in automatically by the compiler.
The last one needs a closer look because assert is a syntactic
form, not a function:
If the precondition holds, assert returns () and
evaluation continues. If it fails, assert raises
Assert_failure (file, line, column) pointing at the failing
line. The special form assert false is an idiom: it always
raises and has the polymorphic type 'a, so you can use it
anywhere to mark a code path as unreachable. Use this when the
type system can't see that a case is impossible (a partial
match you have manually checked, for instance).
There are a handful more stdlib exceptions; the OCaml stdlib documentation lists them under "Predefined exceptions." In practice the six above account for the vast majority of try ... with clauses you will see in idiomatic code.
Custom exceptions with a payload
We have already seen a nullary custom exception
(exception Negative_input) in the Raising section. Custom
exceptions can also carry a payload, declared with the of
keyword the same way as a
variant constructor with arguments:
The first call returns 42 and prints nothing. The second
re-raises a Parse_error (with a message and line number), the
handler catches and binds the payload, prints "line 7: not an
int: oops", and returns 0. The handler pattern
Parse_error (msg, line) binds the constructor's payload
exactly as a variant pattern would.
Extensible variants: a brief aside
Under the hood, all exception constructors share a single type,
exn. Every exception declaration adds a new constructor to
that type. This is unusual: most
OCaml variants are closed (the set of
constructors is fixed at the declaration). exn is one of the
few extensible variants in the language, because libraries
throughout a program need to add their own exception
constructors.
The desugaring is visible if you ask for it. exception NAME of ... is exactly type exn += NAME of ...:
The += form makes the
extension explicit; the exception keyword is the
exception-flavoured shorthand. We will not need to write type exn += ourselves in this course, but seeing it once explains
the bullet "exceptions are extensible variants" in concrete
terms.
Exception vs option vs result
The three shapes for "this might fail," shown on the same tiny lookup over an association list:
The toplevel reports:
val find_x : string -> int = <fun>
val find_x_opt : string -> int option = <fun>
val find_x_result : string -> (int, string) result = <fun>
Same lookup, three signatures. A few calls to feel the difference at the call site:
Raise. Cheapest at the call site: the caller writes
let x = find_x "key" and uses x directly. The cost is that
the type says nothing about failure. A reader of the type
cannot tell whether the function might raise. The compiler will
not warn a caller that forgot to handle the failure.
Option. The failure is in the type. The caller is forced to
pattern-match on Some and None. The cost is two things:
every call site is slightly more code, and the failure carries
no information (None is just "no value here," with no reason).
Result. The same as option, but the failure side has a payload: an error message, an error code, a structured error variant. Best when the caller might want to log or recover based on what went wrong.
The OCaml standard library follows a clear convention: any
function that raises an exception on failure comes in a paired
form that returns option, with the suffix _opt. So
List.find raises Not_found; List.find_opt returns None.
List.assoc raises Not_found; List.assoc_opt returns None.
Hashtbl.find raises; Hashtbl.find_opt returns. The convention
extends to types: the raising form usually exists for historical
reasons, and the _opt form is the one to prefer in new code.
Why two forms? The raising form is older; the _opt form was
added as OCaml moved toward making partial failures visible in
the type system. The library keeps both for backward compatibility,
but the social convention is clear: new code uses _opt.
The naming _opt suffix is the OCaml convention; expect to see
it everywhere.
When not to use exceptions
A short list of cases where reaching for an exception is the wrong call.
For predictable missing values. "The key might be missing from
the map" is not an exceptional case, it is the expected behaviour
of a lookup. Use option;
the caller will pattern-match cleanly.
For "this can't happen" assertions. If a code path is
unreachable, write assert false or, better, restructure the
types so the unreachable case is genuinely impossible (a
variant without that constructor).
Exceptions are not a substitute for type-driven design.
For deeply nested control flow. An exception raised three
levels deep, caught at the top, can be hard to follow when you
read the code. Each try introduces a place where execution can
jump; a function with many trys scattered through it is hard to
reason about.
The genuine sweet spot for exceptions: unexpected, rare failures that callers usually do not handle locally. Parse failures in a parser used inside a larger pipeline, file-system errors that propagate to the top of a CLI, that kind of thing. The convention here matches the convention in Python and Java: exceptions for things that "should not normally happen."
A quick check
What is the type of raise (Failure "oops")?
unitexn'aFailure
Why: raise has type exn -> 'a. Its return type is
polymorphic because a raise never produces a normal value; it
can stand in any type-checking context. The exception value
itself (Failure "oops") has type exn, but the raise
expression has type 'a.
What does this evaluate to?
"no exception""not found""failure"- runtime crash
Why: List.hd [] raises Failure "hd". The Failure _
handler matches and returns "failure". The Not_found clause
does not match (different exception); the body never finishes.
Activity
Write find_first raising Not_found, then write
find_first_opt as a wrapper that calls find_first and
catches Not_found.
Show reference solution
Reference solution:
Show reference solution
The find_first_opt definition is the standard pattern for
turning a raising function into an optional one: wrap the call in
a try, and convert the exception into None. This is exactly
how the standard library's _opt forms are typically defined.
You can also go the other way (a raising version from an optional
version) with a match:
let find_first p xs =
match find_first_opt p xs with
| Some x -> x
| None -> raise Not_found
Either direction works; pick the one whose implementation is easier to read for your case.
What's next
That closes this module's imperative trio. The
next two lectures take a
small detour: streams (infinite data structures, built using
thunks and refs from this module) and memoization (caching
function results, again using a ref to hold the cache). Then
the last three lectures turn to
modules: how OCaml structures code at scale, namespaces large
libraries, and hides representation behind type signatures. The
standard library you have been using all course (List,
Array, String, Option) is a tree of modules; we finally
meet the machinery that builds it.
Reading
- Cornell CS3110, Exceptions: https://cs3110.github.io/textbook/chapters/data/exceptions.html
- Real World OCaml, Error Handling: https://dev.realworldocaml.org/error-handling.html
Sources
This lecture's prose, worked examples, and quizzes are original to
this course. Materials referenced during preparation are listed in
the Reading section above; Cornell CS3110 and Real World OCaml
are CC BY-NC-ND-licensed and have not been derivatively reused.
See LICENSES.md
at the repository root for the full source posture.