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

Exceptions and resource cleanup

Raise and handle OCaml exceptions, define your own with inline records, close files safely with Fun.protect and with_open, and read backtraces.

  • Intermediate
  • 5 minutes
  • Examples run with ocaml 5.5.1, opam 2.6.0, dune 3.24.2 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 5 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Define and raise exceptions with inline records
  • Handle exceptions with try-with and match-exception
  • Protect resources safely with Fun.protect and In_channel.with_open_text
  • Trace errors by reading backtraces for debugging

Before you start

What you’ll learn · Before you start

What you will learn

  • Define and raise exceptions with inline records
  • Handle exceptions with try-with and match-exception
  • Protect resources safely with Fun.protect and In_channel.with_open_text
  • Trace errors by reading backtraces for debugging

Before you start

On this page

A file-reading function opens a channel, reads a line and closes the channel. When the file is empty, input_line raises End_of_file before close_in runs, and the channel stays open. Call the function 100 times on a system that allows 32 open files per process, and the 30th call fails.

You will define and raise exception: A value that travels up the call stack until a handler catches it or the program stops, carrying the name and details of a failure. with inline records, handle them with try ... with and match ... with exception, close resources safely with Fun.protect and In_channel.with_open_text, and read backtrace: A record of the call stack at the point where an exception was raised, showing which functions were active and where. for debugging.

An exception breaks normal flow and carries data

An exception is a value that travels up the call stack until a handler catches it or the program stops. The standard library raises Not_found when a search finds nothing, Invalid_argument when an argument makes no sense, Failure when a function is undefined on its argument, such as int_of_string "12O", and End_of_file when a read reaches the end. Many raising functions have an _opt twin, such as List.assoc_opt and int_of_string_opt, that returns None instead. Not all do: String.sub has no _opt form, so its caller checks the range before the call.

Standard exceptions and their _opt variants Python · conventions_session.py
from session import toplevel

# The standard library's own failures: Not_found for a missing key, Invalid_argument for an argument outside what the
# function accepts, Failure for input it cannot read, each beside the _opt function that returns an option instead.
# Then assert, and an exception used to leave a loop early with raise_notrace.
toplevel("""
let pins = [("Pune", "411001"); ("Kochi", "682001")];;
List.assoc "Delhi" pins;;
List.assoc_opt "Delhi" pins;;
String.sub "MySmartCoPilot" 2 40;;
int_of_string "12O";;
int_of_string_opt "12O";;
List.nth [10; 20; 30] 5;;
List.nth_opt [10; 20; 30] 5;;
assert (List.length pins = 3);;
exception Found of int;;
let first_negative a =
  try Array.iteri (fun i x -> if x < 0 then raise_notrace (Found i)) a; None
  with Found i -> Some i;;
first_negative [| 3; 1; -4; 1; -5 |];;
""")

Output

$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# let pins = [("Pune", "411001"); ("Kochi", "682001")];;
val pins : (string * string) list = [("Pune", "411001"); ("Kochi", "682001")]
# List.assoc "Delhi" pins;;
Exception: Not_found.
# List.assoc_opt "Delhi" pins;;
- : string option = None
# String.sub "MySmartCoPilot" 2 40;;
Exception: Invalid_argument "String.sub / Bytes.sub".
# int_of_string "12O";;
Exception: Failure "int_of_string".
# int_of_string_opt "12O";;
- : int option = None
# List.nth [10; 20; 30] 5;;
Exception: Failure "nth".
# List.nth_opt [10; 20; 30] 5;;
- : int option = None
# assert (List.length pins = 3);;
Exception: Assert_failure ("//toplevel//", 1, 0).
# exception Found of int;;
exception Found of int
# let first_negative a =
    try Array.iteri (fun i x -> if x < 0 then raise_notrace (Found i)) a; None
    with Found i -> Some i;;
    val first_negative : int array -> int option = <fun>
# first_negative [| 3; 1; -4; 1; -5 |];;
- : int option = Some 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 conventions_session.py

assert checks a condition and raises Assert_failure when it is false. raise_notrace raises without recording a backtrace, useful when an exception is used for control flow, such as leaving a loop early with a Found exception.

Key idea

An exception names a failure and carries its details; a handler decides what to do with it.

Checkpoint

Check: Raising and handling exceptions

  1. Question 1 of 2 What does List.assoc "Delhi" [("Pune", "411001"); ("Kochi", "682001")] raise?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Not_found

    List.assoc raises Not_found when the key is missing; List.assoc_opt returns None instead.

  2. Question 2 of 2 What does int_of_string_opt "12O" return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: None

    The _opt variants return None where the plain function would raise.

Define your own exceptions with inline records

An exception defined with exception is a first-class value. Add an of clause to carry data. An inline record puts the fields right in the definition and lets the handler name them directly. Inline records arrived in OCaml 4.03; before that, an exception could carry a record only by naming a record type declared on its own.

A domain exception with an inline record Python · withdraw_session.py
from session import toplevel

# A wallet's withdraw function signals two failures with exceptions: a domain exception whose inline record says how
# much was needed and how much was there, and the standard Invalid_argument for an amount that makes no sense. The
# handlers use match ... with exception, then the older try ... with on the same calls.
toplevel("""
exception Insufficient_funds of { needed : int; available : int };;
let withdraw balance amount =
  if amount <= 0 then invalid_arg "withdraw: amount must be positive"
  else if amount > balance then raise (Insufficient_funds { needed = amount; available = balance })
  else balance - amount;;
withdraw 500 200;;
withdraw 500 800;;
let describe balance amount =
  match withdraw balance amount with
  | left -> Printf.sprintf "paid %d, %d left" amount left
  | exception Insufficient_funds { needed; available } -> Printf.sprintf "short by %d" (needed - available)
  | exception Invalid_argument message -> message;;
List.map (describe 500) [200; 800; 0];;
let describe_try balance amount =
  try Printf.sprintf "paid %d, %d left" amount (withdraw balance amount) with
  | Insufficient_funds r -> Printf.sprintf "short by %d" (r.needed - r.available);;
describe_try 500 800;;
describe_try 500 0;;
""")

Output

$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# exception Insufficient_funds of { needed : int; available : int };;
exception Insufficient_funds of { needed : int; available : int; }
# let withdraw balance amount =
    if amount <= 0 then invalid_arg "withdraw: amount must be positive"
    else if amount > balance then raise (Insufficient_funds { needed = amount; available = balance })
    else balance - amount;;
      val withdraw : int -> int -> int = <fun>
# withdraw 500 200;;
- : int = 300
# withdraw 500 800;;
Exception: Insufficient_funds {needed = 800; available = 500}.
# let describe balance amount =
    match withdraw balance amount with
    | left -> Printf.sprintf "paid %d, %d left" amount left
    | exception Insufficient_funds { needed; available } -> Printf.sprintf "short by %d" (needed - available)
    | exception Invalid_argument message -> message;;
        val describe : int -> int -> string = <fun>
# List.map (describe 500) [200; 800; 0];;
- : string list =
["paid 200, 300 left"; "short by 300"; "withdraw: amount must be positive"]
# let describe_try balance amount =
    try Printf.sprintf "paid %d, %d left" amount (withdraw balance amount) with
    | Insufficient_funds r -> Printf.sprintf "short by %d" (r.needed - r.available);;
    val describe_try : int -> int -> string = <fun>
# describe_try 500 800;;
- : string = "short by 300"
# describe_try 500 0;;
Exception: Invalid_argument "withdraw: amount must be positive".

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 withdraw_session.py

Insufficient_funds { needed; available } says exactly what went wrong. The handler pattern-matches on the exception and on its fields: exception Insufficient_funds { needed; available } binds both at once. match ... with exception checks exceptions and results in one pattern match; try ... with is the older form that catches only exceptions.

Tip

Use match ... with exception when you handle both a result and an exception from the same call. Use try ... with when the work spans several statements or when you want to catch an exception from any of them.

Checkpoint

Check: Defining exceptions with inline records

  1. One question What does the pattern exception Insufficient_funds { needed; available } bind?

    Choose one answer.

    Show the answer: One question

    Answer: Both fields of the exception, needed and available

    Inline records let you name the fields in both the raise site and the handler.

A leaked channel exhausts file descriptors

When code between opening and closing a channel raises an exception, the close never runs, and nothing closes the channel later. The garbage collector may free the channel’s memory, but it leaves the file descriptor open until the process ends; run a program with OCAMLRUNPARAM=W and the runtime warns about each channel that dies without being closed. Under a low limit on open files the leak shows within a second, so the session below sets the limit to 32 with ulimit -n 32.

A leak: End_of_file skips close_in Python · leak_session.py
from session import files, run

# leak.ml closes its file only when input_line succeeds; no_leak.ml lets In_channel.with_open_text close it. Both read
# an empty file 100 times under a limit of 32 open files per process (ulimit -n 32), so a leak shows within the run.
files("leak.ml", "no_leak.ml")
run(
    """
: > empty.txt
ocamlopt leak.ml -o leak
ocamlopt no_leak.ml -o no_leak
ulimit -n 32
./leak
./no_leak
"""
)

Output

$ : > empty.txt
$ ocamlopt leak.ml -o leak
$ ocamlopt no_leak.ml -o no_leak
$ ulimit -n 32
$ ./leak
call 30 failed: Sys_error "empty.txt: Too many open files"
$ ./no_leak
100 calls, no error

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 leak_session.py

The first version opens empty.txt, calls input_line (which raises End_of_file), and never reaches close_in. The 30th call fails with Sys_error "empty.txt: Too many open files": standard input, output and error hold 3 of the 32 descriptors, and 29 leaked channels hold the rest. The second version wraps the read in In_channel.with_open_text, which closes the channel whether the function returns or raises, and finishes all 100 calls.

Key idea

A resource that must be released belongs inside with_open_text, with_open_bin or Fun.protect, not between an open and a close that an exception can skip.

Checkpoint

Check: Resource cleanup with Fun.protect

  1. One question Why did leak.ml fail around call 30, while no_leak.ml finished 100 calls?

    Choose one answer.

    Show the answer: One question

    Answer: leak.ml left channels open when input_line raised End_of_file; no_leak.ml closed them with with_open_text

    with_open_text closes the channel whether the function returns or raises.

Fun.protect runs cleanup whether work succeeds or raises

Fun.protect ~finally work calls work (), then finally (), and returns or re-raises what work did. The cleanup runs even when an exception unwinds the stack. If finally itself raises, its exception arrives wrapped in Fun.Finally_raised so it cannot be mistaken for an exception of work. The ~finally function takes no arguments; it runs for its side effect only.

Fun.protect guarantees the cleanup runs Python · protect_session.py
from session import toplevel

# Fun.protect ~finally work calls work (), then finally (), whether work returned or raised, and passes on work's
# result or exception. The log records the order. When finally itself raises, its exception arrives wrapped in
# Fun.Finally_raised, so it cannot be mistaken for an exception of work.
toplevel("""
let log = ref [];;
let note s = log := s :: !log;;
let finally () = note "finally: release";;
Fun.protect ~finally (fun () -> note "work: returns 42"; 42);;
(try Fun.protect ~finally (fun () -> note "work: raises"; failwith "disk full")
 with Failure m -> note ("caller: caught " ^ m); 0);;
List.rev !log;;
Fun.protect ~finally:(fun () -> failwith "close failed") (fun () -> 42);;
Fun.protect ~finally:(fun () -> failwith "close failed") (fun () -> failwith "disk full");;
""")

Output

$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# let log = ref [];;
val log : '_weak1 list ref = {contents = []}
# let note s = log := s :: !log;;
val note : '_weak1 -> unit = <fun>
# let finally () = note "finally: release";;
val finally : unit -> unit = <fun>
# Fun.protect ~finally (fun () -> note "work: returns 42"; 42);;
- : int = 42
# (try Fun.protect ~finally (fun () -> note "work: raises"; failwith "disk full")
   with Failure m -> note ("caller: caught " ^ m); 0);;
  - : int = 0
# List.rev !log;;
- : string list =
["work: returns 42"; "finally: release"; "work: raises"; "finally: release";
 "caller: caught disk full"]
# Fun.protect ~finally:(fun () -> failwith "close failed") (fun () -> 42);;
Exception: Fun.Finally_raised: Failure("close failed")
# Fun.protect ~finally:(fun () -> failwith "close failed") (fun () -> failwith "disk full");;
Exception: Fun.Finally_raised: Failure("close failed")

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 protect_session.py

The log records the order: work runs first, then finally, then the caller catches the exception. The last two lines show what happens when finally raises: the call ends with Fun.Finally_raised around the cleanup’s Failure("close failed"), whether work returned 42 or raised Failure "disk full", and the exception of work is lost. The documentation of Fun calls a raising finally a programming error, so a cleanup that can fail handles its own exceptions. Reach for Fun.protect when the resource is not a channel: a lock, a temporary file or a connection.

Common mistake

Pass Fun.protect the function, not its result. Fun.protect ~finally (work ()) is rejected unless work () itself returns a function: protect expects a value of type unit -> 'a, and the compiler’s hint asks whether you forgot to wrap the expression in fun () ->. Write Fun.protect ~finally work or Fun.protect ~finally (fun () -> work ()).

Checkpoint

Check: Resource cleanup with Fun.protect

  1. One question Fun.protect ~finally work raises Fun.Finally_raised when which happens?

    Choose one answer.

    Show the answer: One question

    Answer: finally raises, whether work succeeded or raised

    Fun.Finally_raised distinguishes cleanup failures from failures of the work itself.

A backtrace shows where the exception was raised

An uncaught exception prints its message and stops the program with exit status: An integer a program returns to its parent process when it ends, where 0 means success and other values indicate failures. 2; by default no call stack is shown. Setting OCAMLRUNPARAM=b asks the runtime for a backtrace, but a program compiled without -g holds no location information, so every frame reads unknown location. Recompiling with -g makes the same request print the call stack with file names and line numbers.

Without -g, OCAMLRUNPARAM=b has no locations; with -g it prints the stack Python · backtrace_session.py
from session import files, run

# amounts.ml stops with an uncaught Failure. Compiled to bytecode without -g, OCAMLRUNPARAM=b can only ask for a
# backtrace with no location information ("unknown location"); recompiled with -g, the same request prints the call
# stack with file names and lines.
files("amounts.ml")
run(
    """
cat amounts.ml
ocamlc amounts.ml -o amounts
!OCAMLRUNPARAM=b ./amounts
ocamlc -g amounts.ml -o amounts
!OCAMLRUNPARAM=b ./amounts
"""
)

Output

$ cat amounts.ml
(* total adds the amounts of a list of lines. One line has the letter O where a zero belongs, so parse_amount raises
   Failure, nothing handles it, and the program stops. *)
let parse_amount line =
  match int_of_string_opt line with
  | Some n -> n
  | None -> failwith ("not an amount: " ^ line)

let total lines = List.fold_left (fun sum line -> sum + parse_amount line) 0 lines

let () = Printf.printf "total %d\n" (total [ "120"; "80"; "12O" ])
$ ocamlc amounts.ml -o amounts
$ OCAMLRUNPARAM=b ./amounts
Fatal error: exception Failure("not an amount: 12O")
Called from unknown location (inlined)
Called from unknown location (inlined)
Called from unknown location (inlined)
[exit status 2]
$ ocamlc -g amounts.ml -o amounts
$ OCAMLRUNPARAM=b ./amounts
Fatal error: exception Failure("not an amount: 12O")
Raised at Stdlib.failwith in file "stdlib.ml", line 29, characters 17-33
Called from Amounts.total.(fun) in file "amounts.ml", line 8, characters 56-73
Called from Stdlib__List.fold_left in file "list.ml", line 125, characters 24-34
Called from Amounts in file "amounts.ml", line 10, characters 36-66
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 backtrace_session.py

A custom exception can carry its own readable message and record its backtrace from code, without any environment variable. Register a printer with Printexc.register_printer, then call Printexc.record_backtrace true and Printexc.print_backtrace.

A custom exception with a registered printer and a backtrace from code Python · printer_session.py
from session import files, run

# printer.ml registers a printer for its own exception and records and prints the backtrace from code with Printexc,
# so it needs neither OCAMLRUNPARAM nor a caller. Compiled with -g so the backtrace carries file and line numbers.
files("printer.ml")
run(
    """
cat printer.ml
ocamlc -g printer.ml -o printer
!./printer
"""
)

Output

$ cat printer.ml
(* A custom exception gets a readable message from Printexc.register_printer, and the program records and prints the
   backtrace itself with Printexc.record_backtrace and Printexc.print_backtrace, without OCAMLRUNPARAM. *)
exception Insufficient_funds of { needed : int; available : int }

let () =
  Printexc.register_printer (function
    | Insufficient_funds { needed; available } ->
        Some (Printf.sprintf "Insufficient_funds: needed %d, available %d" needed available)
    | _ -> None)

let withdraw balance amount =
  if amount > balance then raise (Insufficient_funds { needed = amount; available = balance });
  balance - amount

let () =
  Printexc.record_backtrace true;
  match withdraw 500 800 with
  | left -> Printf.printf "%d left\n" left
  | exception e ->
      prerr_endline ("error: " ^ Printexc.to_string e);
      Printexc.print_backtrace stderr;
      exit 1
$ ocamlc -g printer.ml -o printer
$ ./printer
error: Insufficient_funds: needed 800, available 500
Raised at Printer.withdraw in file "printer.ml", line 12, characters 27-94
Called from Printer in file "printer.ml", line 17, characters 8-24
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 printer_session.py

Tip

For a long-running program, call Printexc.record_backtrace true at startup and log Printexc.get_backtrace () in the top-level handler, so a crash report names the line that raised. Build it with -g as well, or the report has no file names and line numbers to show.

Checkpoint

Check: Backtraces for debugging

  1. One question What do you need for an uncaught exception to print a backtrace?

    Choose one answer.

    Show the answer: One question

    Answer: Compile with -g and run with OCAMLRUNPARAM=b, or call Printexc.record_backtrace true

    Both compile-time and runtime steps are needed for a useful backtrace.

Practice

Practice that adapts to you

Practise this lesson

7 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 What does match f x with v -> v + 1 | exception Not_found -> 0 return when f x raises Not_found?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: 0

    match ... with exception handles both results and exceptions in one pattern match.

  2. Question 2 of 7 first_negative leaves Array.iteri early with raise_notrace (Found i). Why raise_notrace rather than raise?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: Found carries a result out of the loop, not a failure, so no backtrace is worth recording

    Use raise_notrace when an exception is control flow, such as leaving a loop early with the index found.

  3. Question 3 of 7 Why define an exception with an inline record instead of a tuple?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: The handler can name the fields, making the code clearer

    Named fields document what the exception carries.

  4. Question 4 of 7 In Fun.protect ~finally work, when does finally run?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: After work, whether work returned or raised

    Fun.protect is OCaml's try-finally: the cleanup always runs.

  5. Question 5 of 7 When would you use Fun.protect instead of In_channel.with_open_text?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: When the resource is not a channel, or when you need to handle cleanup failure specially

    Fun.protect is the general form; with_open_text and with_open_bin are shortcuts for channels.

  6. Question 6 of 7 What exit status does an OCaml program return when an exception is not caught?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: 2

    The Stdlib documentation of exit says an implicit exit 2 is performed when a program ends early because of an uncaught exception.

  7. Question 7 of 7 What does Printexc.register_printer do for a custom exception?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: It gives the exception a human-readable string when printed

    A printer returns Some "message" for exceptions it handles and None for others.

Interview questions

What is the difference between List.assoc and List.assoc_opt?

List.assoc raises Not_found when the key is missing; List.assoc_opt returns None instead. Reach for the _opt variant when a missing key is an expected outcome rather than an error, and keep the raising form for a lookup that should never fail.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to senior

    How do you ensure a file channel is closed whether a function succeeds or raises an exception?

    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 staff

    Why would you define an exception with an inline record instead of a tuple?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

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

  1. What does an exception do to a running OCaml program?

    It breaks normal flow, names a failure and carries its details; a handler decides what happens next.

  2. How does your own exception carry several named details?

    With an inline record in its definition, such as exception Insufficient_funds of { needed : int; available : int }, so the handler can name the fields directly. Inline records arrived in OCaml 4.03.

  3. Why does a reader that opens a channel, calls input_line and then close_in leak on an empty file?

    input_line raises End_of_file before close_in runs, so the channel stays open; the descriptor is not released until the process ends.

  4. Where should a resource that must be released live?

    Inside In_channel.with_open_text, with_open_bin or Fun.protect, never between an open and a close that an exception can skip.

  5. Why is Fun.protect ~finally (work ()) rejected?

    Fun.protect expects a value of type unit -> 'a, not the result of the work. Write Fun.protect ~finally work or Fun.protect ~finally (fun () -> work ()).

  6. How do you get a backtrace that names the line that raised?

    Compile with -g and run with OCAMLRUNPARAM=b, or call Printexc.record_backtrace true at start-up and log Printexc.get_backtrace () in the top-level handler.

  7. When is match … with exception the better form, and when try … with?

    match … with exception when you handle both a result and an exception of the same call; try … with when the work spans several statements and an exception from any of them should be caught.

  8. When should code use List.assoc_opt rather than List.assoc?

    When a missing key is an expected outcome: assoc_opt returns None instead of raising Not_found. Keep the raising form for a lookup that should never fail.

Key takeaways

  • An exception names a failure and carries its details; a handler decides what to do with it.
  • Define your own exceptions with inline records to carry structured data.
  • Wrap resources that must be released in with_open_text, with_open_bin or Fun.protect, not between open and close.
  • Compile with -g and run with OCAMLRUNPARAM=b to see backtraces; register a printer for custom exceptions.
  • Use match ... with exception when you handle both a result and an exception from the same call.

Terms in this lesson

backtrace
A record of the call stack at the point where an exception was raised, showing which functions were active and where.
exception
A value that travels up the call stack until a handler catches it or the program stops, carrying the name and details of a failure.
exception handler
A try-with or match-with-exception clause that intercepts an exception and decides what to do with it instead of letting it propagate.
exit status
An integer a program returns to its parent process when it ends, where 0 means success and other values indicate failures.

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.