Exceptions, and catching them
raise stops the current computation with a value, try ... with catches it, and the standard library raises a few exceptions of its own.
An answer that travels upwards
An exception is a value that travels up instead of being returned. raise (Too_big n) abandons the current computation and hands Too_big n to the nearest handler; with no handler anywhere, the program stops and the runtime prints the exception. exception Too_big of int declares one that carries a payload, and exception Missing declares one that carries nothing.
Exceptions are for the cases that should not happen in ordinary use: a broken invariant, a missing file, text that is not a number. A search that can simply fail is better answered with an option, which is why the two ideas sit next to each other in this course. The test is whether the caller can do something useful with the failure; if it can, the failure belongs in the type.
Catch narrowly, and answer
try body with ... runs the body and, if it raises, matches the exception against the handlers — the same pattern syntax as match. The handlers are tried in order, they must agree on the answer type, and a handler may answer a plain value, which is how a failure becomes a friendly string.
The standard library raises its own: Failure from int_of_string, Not_found from List.assoc, and Division_by_zero from an integer division by zero. Catching them by name keeps the unexpected ones fatal, which is what you want while the code is still being written.
Unwinding, and what a handler can see
When raise runs, the expression that raised stops immediately. Every call between the raise and the nearest matching try is abandoned, and none of their remaining work happens. The handler then runs in the environment of the try, not in the function that raised, so it sees only what the try expression can see plus the payload the exception carries.
That is why an exception is good for a broken invariant and poor for an ordinary missing answer. A failure the caller is expected to handle should be in the type, as an option or a result; an exception is for a situation the surrounding code cannot continue past. The standard library follows that split: List.assoc_opt answers an option, while List.assoc raises Not_found.
A try only covers the expression before with. If the body is a sequence, the whole sequence is inside the try; if the try is an argument to a function, only that argument is covered. Brackets decide the boundary, and a handler that seems not to catch a failure is usually a handler wrapped around too little.
How the handler is chosen
A try installs a handler for the dynamic extent of its body: every call made while the body runs, however deep, can raise to it. The runtime walks back up the call stack until it finds the most recent handler that matches the exception’s constructor. That is why an exception can escape a function that never mentions it, and why a function’s type does not list the exceptions it might raise.
This is the trade. The type system checks values, not effects, so an exception is invisible in a signature. A function that raises Not_found and one that returns an option look the same to a caller reading only the type — which is the argument for putting expected failures in the return type.
What goes wrong
The catch-all is the first. with _ -> 0 turns every failure into zero: a Failure, a Not_found, a bug in a helper, all indistinguishable. When the code later prints a wrong number, there is no trace of what went wrong. Name the exceptions you expect, and let the others stop the run.
The second is a handler that catches too little. try print_endline (int_of_string s) with Failure _ -> ... covers the whole expression, including the conversion; print_endline (try int_of_string s with Failure _ -> 0) covers only the conversion. Where the try sits decides what it protects, and brackets are the boundary.
Three exception mistakes: a catch-all with _ -> ... turns a typo or a bug into a quiet wrong answer; try f x with ... covers the call but not arguments evaluated outside it; and an exception raised while building the handler's own answer is not caught by that handler. Catch the exceptions you expect, by name, and let the rest stop the program.
Try these next
- Write a
safe_headthat raises a customEmptyexception, then catch it and print a message. - Use
try List.assoc key pairs with Not_found -> ...for a missing key without letting the exception escape. - Add a second custom exception, raise it inside the same
try, and confirm theFailurehandler lets it through.
A lookup that raises its own exception
exception Missing of string
let find key pairs =
try List.assoc key pairs
with Not_found -> raise (Missing key)
let () =
try print_endline (string_of_int (find "a" []))
with Missing key -> print_endline (key ^ " is missing")Too big, not a number, divided by zero
Run it and read the five lines. Then change the limit so that 4 is too big, and try `int_of_string "4 2"` in place of `42` to see which exception arrives.
A lookup with two failures
Run it and read the lines. Then add a `Too_short` exception for keys shorter than two letters, raise it in `find`, and catch it in one of the calls.
A handler for every exception hides the bugs you wanted to see. Listing Failure _ and Division_by_zero separately leaves the unexpected ones fatal, and the underscore in a pattern is what says the payload is not needed.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does raise do?
