Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 15 minutes
  • Examples run with ocaml 5.5.1, opam 2.6.0, dune 3.24.2 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 4 of 15 minutes free
Free: Result and option carry a failure as a value · A chain of result steps stops at the first error · Writing the chain with let-star from Result.Syntax · Practise: result, option and let-star. With a pass: 3 in-depth parts and 3 interview answers, about 11 minutes.

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

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

  1. One question A lookup in a list of settings returns the value or says the key is missing. Which return type carries this failure best?

    Choose one answer.

    Show the answer: One question

    Answer: string option, because the only reason is that there is nothing

    Reach for option when the reason adds nothing and result when the reason matters. The session's parse_age returns a result, with Not_a_number or Age_out_of_range, because the UI needs to know which one.

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:

Interactive · stage by stage One submission through three validate steps Step through the four submissions. An Error at any step goes straight to the result and skips the rest.
One submission through three validate steps, step 5 of 5. Showing validate_name (Ok "Asha"), parse_age (Ok 25), parse_pin (Bad_pin "012345"), signup returns (Bad_pin "012345"). In focus: signup returns.error skips the resterror skips the resterror skips the restvalidate_nameOk "Asha"parse_ageOk 25parse_pinBad_pin "012345"signup returnsBad_pin "012345"

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Asha, 25, 560001. Every step returned Ok, so signup returned Ok ("Asha", 25, "560001"): the Ok triple of the three parsed values.
  2. 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.
  3. 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.
  4. Asha, 25, 012345. The first two steps succeeded. parse_pin saw a leading zero and returned Error (Bad_pin "012345").
  5. 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").

Four submissions through validate_name, parse_age and parse_pin Python · signup_session.py
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

  1. One question validate_name returns (string, signup_error) result; String.length also returns int. Which of Result.map and Result.bind chains them to compute the name's length?

    Choose one answer.

    Show the answer: One question

    Answer: Result.map, because String.length cannot fail

    Result.map suits a step that always succeeds (uppercase, length); Result.bind suits a step that returns a result (parse_age, parse_pin).

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

  1. One question Which line turns every later let* in the file into Result.bind on an OCaml compiler that has no Result.Syntax?

    Choose one answer.

    Show the answer: One question

    Answer: let ( let* ) = Result.bind

    let ( let* ) = Result.bind is one line and is what the session's signup_classic uses; it works on any OCaml version with binding operators (OCaml 4.08 and later).

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.

  1. Question 1 of 6 In OCaml 5.5, what are the two constructors of the result type?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: Ok of 'a and Error of 'e

    Result has two type parameters, one for the value and one for the error, written ('a, 'e) result.

  2. Question 2 of 6 What is the type of Result.map (fun s -> String.length s) (validate_name "Asha")?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: (int, signup_error) result

    Result.map takes a function from the value type to a new type, and keeps the error type.

  3. Question 3 of 6 For the submission ("Asha", "twenty", "012345"), which error did signup return?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: Error (Not_a_number "twenty")

    Result.bind stops at the first Error, so signup returned the first failing step's error.

  4. Question 4 of 6 What is Result.bind (Ok 7) (fun n -> Ok (n + 1))?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: Ok 8

    bind r f is f v if r is Ok v, and r itself if r is Error _.

  5. Question 5 of 6 What does let open Result.Syntax in let* x = Ok 3 in Ok (x + 1) return?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: Ok 4

    Result.Syntax defines let* as Result.bind, so one let* line is one call of bind.

  6. Question 6 of 6 A code base must build on OCaml 5.3 and 5.5. How do you keep using let* for Result.bind?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: Add let ( let* ) = Result.bind in each file that chains results

    Result.Syntax arrived in OCaml 5.4; before then, define the operator yourself with one line.

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.

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.

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.

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

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

  • result carries the reason a step failed as a value the type checker will not let the caller drop; option carries a failure that has nothing to say about itself.
  • Result.bind, as let* from Result.Syntax since OCaml 5.4, chains fallible steps so that the first Error reaches the caller untouched; let ( let* ) = Result.bind is the one-line fallback on older compilers.
  • Result.map applies a function inside Ok and leaves Error alone; Result.get_ok raises Invalid_argument "result is Error _", so reach for a match or Result.value ~default instead.

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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.