OCaml Backtraces on Uncaught Exceptions
Uncaught exception: Not_found
This blog post probably won't teach anything new to OCaml veterans; but for the others, you might be glad to learn that this very basic, yet surprisingly little-known feature of OCaml will give you backtraces with source file positions on any uncaught exception.
Since it can save hours of frustrating debugging, my intent is to give some publicity to this accidentally hidden feature.
PSA: define
OCAMLRUNPARAM=b
in your environment.
For those wanting to go further, I'll then go on with hints and guidelines for good exception management in OCaml.
For the details, everything here is documented in the Printexc module.
Get your stacktraces!
Compile-time errors are good, but sometimes you just have to cope with run-time failures.
Here is a simple (and buggy) program:
let dict = [
"foo", "bar";
"foo2", "bar2";
]
let rec replace = function
| [] -> []
| w :: words -> List.assoc w dict :: words
let () =
let words = Array.to_list Sys.argv in
List.iter print_endline (replace words)
Side note
For purposes of the example, we use
List.assoc
here; this relies on OCaml's structural equality, which is often a bad idea in projects, as it can break in surprising ways when the matched type gets more complex. A more serious implementation would use e.g.Map.Make
with an explicit comparison function.
Here is the result of executing this program with no options:
$ ./foo
Fatal error: exception Not_found
This isn't very helpful, but no need for a debugger, lots of printf
or tedious
debugging, just do the following:
$ export OCAMLRUNPARAM=b
$ ./foo
Fatal error: exception Not_found
Raised at Stdlib__List.assoc in file "list.ml", line 191, characters 10-25
Called from Foo.replace in file "foo.ml", line 8, characters 18-35
Called from Foo in file "foo.ml", line 12, characters 26-41
Much more helpful! In most cases, this will be enough to find and fix the bug.
If you still don't get the backtrace, you may need to recompile with -g
(with
dune, ensure your default profile is dev
or specify --profile=dev
)
So, now we know where the failure occured... But not on what input. This is
not a matter of backtraces: if that's an issue, define your own exceptions,
with arguments, and raise that rather than the basic Not_found
.
Hint
If you run the program directly from your editor, with a properly configured OCaml mode, the file positions in the backtrace should be parsed and become clickable, making navigation very quick and easy.
Improve your traces
The above works well in general, but depending on the complexity of the programs, there are some more advanced tricks that may be helpful, to preserve or improve the backtraces.
Properly Re-raising exceptions, and finalisers
It's pretty common to want a finaliser after some processing, here to remove a temporary file:
let with_temp_file basename (f: unit -> 'a) : 'a =
let filename = Filename.temp_file basename in
match f filename with
| result ->
Sys.remove filename;
result
| exception e ->
Sys.remove filename;
raise e
In simple cases this will work, but if e.g. you are using the Printf
module
before re-raising, it will break the printed backtrace.
-
Solution 1: use
Fun.protect ~finally f
that handles the backtrace properly. -
Solution 2: manually, use raw backtrace access from the
Printexc
module:| exception e -> let bt = Printexc.get_raw_backtrace () in Sys.remove filename; Printexc.raise_with_backtrace e bt
Re-raising exceptions after catching them should always be done in this way.
There are holes in my backtrace!
Indeed, it may appear that not all function calls show up in the backtrace.
There are two main reasons for that:
- functions can get inlined by the compiler, so they don't actually appear in the concrete backtrace at runtime;
- tail-call optimisation also affects the stack, which can be visible here;
Don't run and disable all optimisations though! Some effort has been put in recording useful debugging information even in these cases. The Flambda pass of the compiler, which does more inlining, also actually makes it more traceable.
As a consequence, switching to Flambda will often give you more helpful
backtraces with recursive functions and tail-calls. It can be done with opam install ocaml-option-flambda
(this will recompile the whole opam switch).
Well, what if my program uses
lwt
?Backtraces in this context are a complex matter -- but they can be simulated: a good practice is to use
ppx_lwt
and thelet%lwt
syntax rather thanlet*
orLwt.bind
directly, because the ppx will insert calls that reconstruct "fake" backtrace information.
Guidelines for exception handling, and Control-C
Exceptions in OCaml can happen anywhere in the program: besides uses of raise
,
system errors can trigger them. In particular, if you want to implement clean
termination on the user pressing Control-C
without manually handling signals,
you should call Sys.catch_break true
; you will then get a Sys.Break
exception raised when the user interrupts the program.
Anyway, this is one reason why you must never use try .. with _ ->
let find_opt x m =
try Some (Map.find x m)
with _ -> None
The programmer was too lazy to write with Not_found
. They may think this is OK
since Map.find
won't raise anything else. But if Control-C
is pressed at the
wrong time, this will catch it, and return None
instead of stopping the
program !
let find_debug x m =
try Map.find x m
with e ->
let bt = Printexc.get_raw_backtrace () in
Printf.eprintf "Error on %s!" (to_string x);
Printexc.raise_with_backtrace e bt
This version is OK since it re-raises the exception. If you absolutely need to catch all exceptions, a last resort is to explicitely re-raise "uncatchable" exceptions:
let this_is_a_last_resort =
try .. with
| (Sys.Break | Assert_failure _ | Match_failure _) as e -> raise e
| _ -> ..
In practice, you'll finally want to catch exceptions from your main function
(cmdliner
already offers to do this, for example); catching Sys.Break
at
that point will offer a better message than Uncaught exception
, give you
control over finalisation and the final exit code (the convention is to use
130
for Sys.Break
).
Controlling the backtraces from OCaml
Setting OCAMLRUNPARAM=b
in the environment works from the outside, but the
module Printexc
can also be used to enable or disable them from the OCaml program itself.
Printexc.record_backtrace: bool -> unit
toggles the recording of backtraces. Forcing itoff
when running tests, oron
when a debug flag is specified, can be good ideas;Printexc.backtrace_status: unit -> bool
checks if recording is enabled. This can be used when finalising the program to print the backtraces when enabled;
Nota Bene
The
base
library turnson
backtraces recording by default. While I salute an attempt to remedy the issue that this post aims to address, this can lead to surprises when just linking the library can change the output of a program (e.g. this might require specific code for cram tests not to display backtraces)
The Printexc
module also allows to register custom exception printers: if,
following the advice above, you defined your own exceptions with parameters, use
Printexc.register_printer
to have that information available when they are
uncaught.
About OCamlPro:
OCamlPro is a R&D lab founded in 2011, with the mission to help industrial users benefit from experts with a state-of-the-art knowledge of programming languages theory and practice.
- We provide audit, support, custom developer tools and training for both the most modern languages, such as Rust, Wasm and OCaml, and for legacy languages, such as COBOL or even home-made domain-specific languages;
- We design, create and implement software with great added-value for our clients. High complexity is not a problem for our PhD-level experts. For example, we helped the French Income Tax Administration re-adapt and improve their internally kept M language, we designed a DSL to model and express revenue streams in the Cinema Industry, codename Niagara, and we also developed the prototype of the Tezos proof-of-stake blockchain from 2014 to 2018.
- We have a long history of creating open-source projects, such as the Opam package manager, the LearnOCaml web platform, and contributing to other ones, such as the Flambda optimizing compiler, or the GnuCOBOL compiler.
- We are also experts of Formal Methods, developing tools such as our SMT Solver Alt-Ergo (check our Alt-Ergo Users' Club) and using them to prove safety or security properties of programs.
Please reach out, we'll be delighted to discuss your challenges: contact@ocamlpro.com or book a quick discussion.
Most Recent Articles
2024
- opam 2.3.0 release!
- Optimisation de Geneweb, 1er logiciel français de Généalogie depuis près de 30 ans
- Alt-Ergo 2.6 is Out!
- Flambda2 Ep. 3: Speculative Inlining
- opam 2.2.0 release!
- Flambda2 Ep. 2: Loopifying Tail-Recursive Functions
- Fixing and Optimizing the GnuCOBOL Preprocessor
- OCaml Backtraces on Uncaught Exceptions
- Opam 102: Pinning Packages
- Flambda2 Ep. 1: Foundational Design Decisions
- Behind the Scenes of the OCaml Optimising Compiler Flambda2: Introduction and Roadmap
- Lean 4: When Sound Programs become a Choice
- Opam 101: The First Steps
2023
- Maturing Learn-OCaml to version 1.0: Gateway to the OCaml World
- The latest release of Alt-Ergo version 2.5.1 is out, with improved SMT-LIB and bitvector support!
- 2022 at OCamlPro
- Autofonce, GNU Autotests Revisited
- Sub-single-instruction Peano to machine integer conversion
- Statically guaranteeing security properties on Java bytecode: Paper presentation at VMCAI 23
- Release of ocplib-simplex, version 0.5