OCaml Module 8 – Errors, input/output and command-line programs
Result, option and binding operators
Return OCaml errors as values with result and option, chain fallible steps with let* from Result.Syntax, and design error types with variants.
What you will learn
- Report errors as values with result and option
- Combine fallible steps with let* from Result.Syntax
- Design error types with closed or polymorphic variants
- Choose between exceptions and results for an API
Before you start
What you’ll learn · Before you start
What you will learn
- Report errors as values with result and option
- Combine fallible steps with let* from Result.Syntax
- Design error types with closed or polymorphic variants
- Choose between exceptions and results for an API
Before you start
On this page
A sign-up form takes a name, an age and a 6-digit PIN code, and 4 things can go wrong in one submission: the name
may be too short, the age not a whole number, the age outside 1 to 120, or the PIN not a valid 6 digits. OCaml’s
result and option types carry each failure back as a value the type checker refuses to let the caller forget,
instead of raising an exception that a missing handler turns into a stopped program.
You will report errors with the standard result and option types, combine fallible steps with let* from
Result.Syntax, design error types with closed or polymorphic variants, and choose between exceptions and results
for an API.
Result and option carry a failure as a value
The result: The OCaml standard variant type ('a, 'e) result = Ok of 'a | Error of 'e, used to return either an answer or a reason there is none; Result.bind and Result.map compose steps that return it. type, ('a, 'e) result = Ok of 'a | Error of 'e, is the standard way to
return either an answer or a reason there is none. Each step of a sign-up for an Indian user returns one:
validate_name gives Ok name or Error Short_name, parse_age gives Ok n or Error (Not_a_number s) or
Error (Age_out_of_range n), and parse_pin checks the 6-digit PIN code (UPU guide, PDF)
with Error (Bad_pin s) when the text is not exactly 6 digits or, as this form also requires, starts with 0.
The option: The standard variant type 'a option = None | Some of 'a, used for a result that may be missing instead of a null value. More in “Variants, options and recursive types” type carries a failure that has nothing to say about itself:
List.assoc_opt returns None when the key is missing because the only reason is that nothing was there, and
int_of_string_opt does the same for a text that is not a number. An option is either Some v or None, and nothing else. Reach for result when the reason matters, and
option when the caller only needs to know whether there is an answer. Convert from one to the other with
Result.to_option and Option.to_result ~none:
| Carrier | Shape | Use it when |
|---|---|---|
option |
None or Some v |
The reason adds nothing: a key is missing, text is not a number |
result |
Ok v or Error e |
The reason matters: the caller must say what went wrong |
Result.map f r applies f inside an Ok; an Error passes through unchanged. The session below runs
validate_name through Result.map String.uppercase_ascii, which turns Ok "Asha" into Ok "ASHA" and leaves
Error Short_name as it is.
Checkpoint
Check: Carrying a failure in result or option
A chain of result steps stops at the first error
Result.bind o f is f v when o is Ok v, and o itself when it is Error _. One call of it per step chains
the three validate steps, so that an Error from any one of them skips the rest and reaches the caller untouched. Step through
signup on the four submissions the session runs:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Asha, 25, 560001. Every step returned Ok, so signup returned Ok ("Asha", 25, "560001"): the Ok triple of the three parsed values.
- A, 25, 560001. validate_name saw a one-character name and returned Error Short_name. Result.bind does not call the next function on an Error, so parse_age and parse_pin never ran.
- Asha, twenty, 560001. validate_name returned Ok "Asha", so parse_age ran; int_of_string_opt returned None for "twenty", and parse_age returned the error. parse_pin never ran.
- Asha, 25, 012345. The first two steps succeeded. parse_pin saw a leading zero and returned Error (Bad_pin "012345").
- One chain, four outcomes. The list of all four results in one line of OCaml: Ok ("Asha", 25, "560001"), Error Short_name, Error (Not_a_number "twenty"), Error (Bad_pin "012345").
from session import toplevel
# The free core: a sign-up form takes a name, an age and a 6-digit PIN code (India Post postal index number).
# Each step may fail. We carry each failure as a value with Result.t, chain the three steps with Result.Syntax
# let* in OCaml 5.5.1, and also show the one-line fallback (let ( let* ) = Result.bind) for older compilers.
toplevel("""
type signup_error =
| Short_name
| Not_a_number of string
| Age_out_of_range of int
| Bad_pin of string
;;
let validate_name s =
if String.length s >= 2 then Ok s else Error Short_name
;;
let parse_age s =
match int_of_string_opt s with
| None -> Error (Not_a_number s)
| Some n when n >= 1 && n <= 120 -> Ok n
| Some n -> Error (Age_out_of_range n)
;;
let parse_pin s =
let digits_only = String.for_all (fun c -> c >= '0' && c <= '9') s in
if String.length s = 6 && digits_only && s.[0] <> '0'
then Ok s
else Error (Bad_pin s)
;;
let signup name age pin =
let open Result.Syntax in
let* n = validate_name name in
let* a = parse_age age in
let* p = parse_pin pin in
Ok (n, a, p)
;;
let cases = [
("Asha", "25", "560001");
("A", "25", "560001");
("Asha", "twenty", "560001");
("Asha", "25", "012345");
];;
List.map (fun (n, a, p) -> signup n a p) cases;;
(* Older OCaml compilers (before 5.4) have no Result.Syntax, so define let* yourself. *)
let ( let* ) = Result.bind;;
let signup_classic name age pin =
let* n = validate_name name in
let* a = parse_age age in
let* p = parse_pin pin in
Ok (n, a, p)
;;
List.map (fun (n, a, p) -> signup_classic n a p) cases;;
(* Result.map changes a value inside Ok; Error passes through unchanged. *)
Result.map String.uppercase_ascii (validate_name "Asha");;
Result.map String.uppercase_ascii (validate_name "A");;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# type signup_error =
| Short_name
| Not_a_number of string
| Age_out_of_range of int
| Bad_pin of string
;;
type signup_error =
Short_name
| Not_a_number of string
| Age_out_of_range of int
| Bad_pin of string
#
let validate_name s =
if String.length s >= 2 then Ok s else Error Short_name
;;
val validate_name : string -> (string, signup_error) result = <fun>
#
let parse_age s =
match int_of_string_opt s with
| None -> Error (Not_a_number s)
| Some n when n >= 1 && n <= 120 -> Ok n
| Some n -> Error (Age_out_of_range n)
;;
val parse_age : string -> (int, signup_error) result = <fun>
#
let parse_pin s =
let digits_only = String.for_all (fun c -> c >= '0' && c <= '9') s in
if String.length s = 6 && digits_only && s.[0] <> '0'
then Ok s
else Error (Bad_pin s)
;;
val parse_pin : string -> (string, signup_error) result = <fun>
#
let signup name age pin =
let open Result.Syntax in
let* n = validate_name name in
let* a = parse_age age in
let* p = parse_pin pin in
Ok (n, a, p)
;;
val signup :
string -> string -> string -> (string * int * string, signup_error) result =
<fun>
#
let cases = [
("Asha", "25", "560001");
("A", "25", "560001");
("Asha", "twenty", "560001");
("Asha", "25", "012345");
];;
val cases : (string * string * string) list =
[("Asha", "25", "560001"); ("A", "25", "560001");
("Asha", "twenty", "560001"); ("Asha", "25", "012345")]
#
List.map (fun (n, a, p) -> signup n a p) cases;;
- : (string * int * string, signup_error) result list =
[Ok ("Asha", 25, "560001"); Error Short_name; Error (Not_a_number "twenty");
Error (Bad_pin "012345")]
#
(* Older OCaml compilers (before 5.4) have no Result.Syntax, so define let* yourself. *)
let ( let* ) = Result.bind;;
val ( let* ) : ('a, 'b) result -> ('a -> ('c, 'b) result) -> ('c, 'b) result =
<fun>
#
let signup_classic name age pin =
let* n = validate_name name in
let* a = parse_age age in
let* p = parse_pin pin in
Ok (n, a, p)
;;
val signup_classic :
string -> string -> string -> (string * int * string, signup_error) result =
<fun>
#
List.map (fun (n, a, p) -> signup_classic n a p) cases;;
- : (string * int * string, signup_error) result list =
[Ok ("Asha", 25, "560001"); Error Short_name; Error (Not_a_number "twenty");
Error (Bad_pin "012345")]
#
(* Result.map changes a value inside Ok; Error passes through unchanged. *)
Result.map String.uppercase_ascii (validate_name "Asha");;
- : (string, signup_error) result = Ok "ASHA"
# Result.map String.uppercase_ascii (validate_name "A");;
- : (string, signup_error) result = Error Short_name
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 signup_session.py
The nested-match version of the same chain needs an Error e -> Error e case for every step: nine lines for three
steps, where Result.bind writes each step as one line and the chain ends with one Ok. Result.map suits a step that always succeeds (the uppercase
example above); Result.bind suits a step that itself returns a result. Mapping a step that returns a result
gives a result result, a sign that bind was meant.
Reading an Error with Result.get_ok
Wrong: let name = Result.get_ok (validate_name "A"), which raises Invalid_argument "result is Error _".
Right: match on the result (match validate_name s with Ok n -> ... | Error Short_name -> ...), or use
Result.value ~default to fall back when a default makes sense.
Checkpoint
Check: A chain of result steps stops at the first Error
Writing the chain with let-star from Result.Syntax
Binding operators are OCaml functions that look like keywords, so a library can give let* the meaning of a chaining function for its own type. The Result.Syntax module (available in OCaml 5.4 and later) defines the binding operator: An operator such as let* that the programmer defines and OCaml uses to read let* x = e in body as a call of that operator on e and fun x -> body; defined as Option.bind, it chains steps that may give None. More in “Folds over trees, options and other structures” let* and let+, where let* reads as Result.bind, so the signup chain becomes a sequence of let* lines that
stops at the first Error. OCaml rewrites let* x = e1 in e2 into ( let* ) e1 (fun x -> e2). let open Result.Syntax in let* name = validate_name ... in let* age = ... in let* pin = ... in Ok (name, age, pin) is the chain of Result.binds the session’s first signup runs. Option.Syntax
(available in OCaml 5.5 and later) does the same for options and defines binding operators for Option.bind.
An older compiler without Result.Syntax, from OCaml 4.08, which introduced binding operators, to 5.3, reads let*
from the programmer: let ( let* ) = Result.bind turns
every later let* in the file into the same chain, with no other code changes. The session’s signup_classic
redefines the binding operator and gives the same four outputs as signup. Result.Syntax also defines let+ and
and+, which combine independent results into one and keep only the first error — a parallel validator that lists
every error of a submission takes a different shape, and so does an error type that must stay open as more parsers
are added. The choice between exceptions and result for an API follows how often the step fails and how far the
caller is: the parts below work out each of these.
Key idea
Result.bind, written as let* from Result.Syntax, chains fallible steps so that the first Error reaches the
caller untouched and every later step is skipped.
Checkpoint
Check: let* from Result.Syntax, and the one-line fallback
Practise: result, option and let-star
Six questions, two on each idea of this part: result and option, the chain of binds, and let*.
Practice that adapts to you
Practise this lesson
6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Designing error types: closed variants, polymorphic variants and the chain's inferred type Pro: opens with a Pro, Premium or Ultimate pass
An error type lets the compiler check that every caller handles each failure, and a new case the parser can produce reaches every match on it. OCaml offers two kinds of variant for an error type, and the choice follows how many callers write their own match on it and how often new cases are added.
In this part:
- Closed variants spell every case out
- Polymorphic variants grow with the chain
- Which shape fits which code
1 runnable example (Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
let+ and and+ for independent results, with every error of a submission reported together Pro: opens with a Pro, Premium or Ultimate pass
A sign-up form is submitted once, and the user wants every mistake listed in one message, not one at a time. The
let+ and and+ of Result.Syntax combine independent results, but their underlying Result.product keeps only
the first error, so a parallel validator that reports every error takes a different shape.
In this part:
- and+ keeps only the first error
- A parallel validator that keeps every error
- A field-named error for the UI
- When the chain still wins
3 runnable examples (ML, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Choosing exceptions or results for an API, with the monad laws checked on real values Pro: opens with a Pro, Premium or Ultimate pass
The choice between an exception and a result for a function’s return type sets what every caller must write. An
exception that is never caught is a stopped program; a result that a caller uses as if it were the plain value is a
type error.
In this part:
- The same parser in two styles
- Choosing by failure rate and callers
- The monad laws, checked on real values
- A worked migration plan
1 runnable example (Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Mid to senior
A team starts a library that parses records and reports why one is invalid. One member wants a closed variant for the error, another wants polymorphic variants that each parser infers. Choose for them, and say what the choice costs when they add a new error case six months later.
-
Mid to senior
A sign-up service validates name, age and PIN with a let* chain in Result.Syntax, and users complain that only one mistake is shown at a time. Rewrite the validator so that one submission with three bad fields reports every error in one go, and say what you lose from the chain.
-
Mid to senior
A JSON parsing library returns a value or raises Not_a_json. A caller that reads a stream of records wants to report which record failed and keep going. Choose a return type and explain the migration without breaking the current callers at once.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How do result and option differ as ways of returning a failure?
result carries the reason a step failed, Error e, as a value the type checker will not let the caller drop; option reports a failure that has nothing to say about itself, None.
-
What happens in a chain of result steps when one of them fails?
The first Error reaches the caller untouched, and every later step is skipped.
-
What is let* in Result.Syntax, and what replaces it on older compilers?
Result.bind written as a binding operator, in Result.Syntax since OCaml 5.4. On older compilers, let ( let* ) = Result.bind is the one-line fallback.
-
Why avoid Result.get_ok on a value that may be an Error?
It raises Invalid_argument "result is Error _". Match on the result, or use Result.value ~default when a default makes sense.
-
What does Result.map do to an Ok and to an Error?
It applies the function inside Ok and leaves an Error exactly as it was.
-
Why return a form's validation failures as values rather than raise them?
Each failure comes back as a value the type checker refuses to let the caller forget, while an exception that nobody handles stops the program.
Key takeaways
resultcarries the reason a step failed as a value the type checker will not let the caller drop;optioncarries a failure that has nothing to say about itself.Result.bind, aslet*fromResult.Syntaxsince OCaml 5.4, chains fallible steps so that the firstErrorreaches the caller untouched;let ( let* ) = Result.bindis the one-line fallback on older compilers.Result.mapapplies a function insideOkand leavesErroralone;Result.get_okraisesInvalid_argument "result is Error _", so reach for a match orResult.value ~defaultinstead.
Terms in this lesson
- monad
- A type with two operations: return, which wraps a plain value, and bind, which chains a step that returns a value of the same type. Chains of steps that may fail or wait are often written this way.
- result
- The OCaml standard variant type ('a, 'e) result = Ok of 'a | Error of 'e, used to return either an answer or a reason there is none; Result.bind and Result.map compose steps that return it.
References
- The OCaml Manual: module Result (Inria and the OCaml developers)
- The OCaml Manual: module Option (Inria and the OCaml developers)
- The OCaml Manual: Binding operators (Inria and the OCaml developers)
- The OCaml Manual: Polymorphic variants (Inria and the OCaml developers)
- Error Handling (OCaml.org)
- Monads (OCaml.org)
- Postal addressing systems in member countries: India (Universal Postal Union)
Report a problem with this lesson
Kept only in this browser. Your Learn progress