Module Err

Err is an abstraction to report located errors and warnings to the user.

The canonical syntax for an error produced by this lib is:

File "my-file", line 42, character 6-11:
42 | Hello World
           ^^^^^
Error: Some message that gives a general explanation of the issue.
Followed by more details.

It is heavily inspired by dune's user_messages and uses dune's error message rendering under the hood.

type t

A value of type t is an immutable piece of information that the programmer intends to report to the user, in a way that breaks flow. To give some concrete examples:

  • A message (or several) reporting an error condition in a program
  • A request to exit the program with an exit code.
exception E of t

The exception that is raised to break the control flow of programs using Err.

Examples:

  • When reporting an error:

    if had_error then Err.raise [ Pp.text "An error occurred" ]
  • When requesting an exit code:

    if shall_exit_42 then Err.exit 42

The standard usage for this library is to wrap entire sections in a protect, which takes care of catching E and handling it accordingly. You may also catch this exception manually if you need, for more advanced uses (in which case you'll take care of recording and re-raising backtraces, etc).

val sexp_of_t : t -> Sexplib0.Sexp.t

Return a Sexp to inspect what inside t. Note the sexp is incomplete and it is not meant for supporting any kind of round trip serialization (there is no t_of_sexp). Rather, this is for quick inspection of what's inside t. We think exposing this can help accommodating some use cases, making it easier to write expect tests involving Err, etc.

Exit codes

This part allows breaking the control flow with an exception indicating a request to end the program with a given exit code.

module Exit_code : sig ... end

The handling of exit code is based on Cmdliner conventions.

val exit : Exit_code.t -> _

Request the termination of the program with the provided exit code. Make sure you have documented that particular exit code in the man page of your CLI. We recommend to stick to the error codes exposed by Exit_code, which are documented by default and pervasively used by existing CLIs built with cmdliner. Raises E.

Raising errors

module Style : sig ... end

You may decorate the messages built with this library with styles that will make them look awesome in a console. Enhance your users experience today!

val raise : ?loc:Loc.t -> ?hints:Style.t Pp.t list -> ?exit_code:Exit_code.t -> Style.t Pp.t list -> _

Raise a user error. You may override exit_code with the requested exit code to end the program with. It defaults to Exit_code.some_error.

Example:

let unknown_var var =
  Err.raise
    ~loc
    [ Pp.textf "Unknown variable '%s'" var ]
    ~hints:(Err.did_you_mean var ~candidates:[ "foo"; "bar"; "baz" ])
;;
val reraise : Stdlib.Printexc.raw_backtrace -> t -> ?loc:Loc.t -> ?hints:Style.t Pp.t list -> ?exit_code:Exit_code.t -> Style.t Pp.t list -> _

Reraise with added context. Usage:

match do_x (Y.to_x y) with
| exception Err.E e ->
  let bt = Printexc.get_raw_backtrace () in
  Err.reraise bt e [ Pp.text "Trying to do x with y"; Y.pp y ]
val create : ?loc:Loc.t -> ?hints:Style.t Pp.t list -> ?exit_code:Exit_code.t -> Style.t Pp.t list -> t

Create a err and return it, instead of raising it right away.

val append : ?exit_code:Exit_code.t -> t -> t -> t

append ?exit_code t1 t2 creates a new err that contains all messages of t1 and t2. The exit_code of this new t may be specified, otherwise it will be that of t2.

val of_stdune_user_message : ?exit_code:Exit_code.t -> Stdune.User_message.t -> t

This is exposed to help with libraries compatibility if needed.

Result

val ok_exn : ('a, t) Stdlib.result -> 'a

Helper to raise a user error from a result type.

  • ok_exn (Ok x) is x
  • ok_exn (Error msg) is Stdlib.raise (E msg)

Other styles

Whether that is during a migration, or to keep experimenting, we are currently exploring other ways to build and raise errors, using things like sexp, json or dyn.

val create_s : ?loc:Loc.t -> ?hints:Style.t Pp.t list -> ?exit_code:Exit_code.t -> string -> Sexplib0.Sexp.t -> t
val raise_s : ?loc:Loc.t -> ?hints:Style.t Pp.t list -> ?exit_code:Exit_code.t -> string -> Sexplib0.Sexp.t -> _
val reraise_s : Stdlib.Printexc.raw_backtrace -> t -> ?loc:Loc.t -> ?hints:Style.t Pp.t list -> ?exit_code:Exit_code.t -> string -> Sexplib0.Sexp.t -> _

Reraise with added context. Usage:

match do_x x with
| exception Err.E e ->
  let bt = Printexc.get_raw_backtrace () in
  Err.reraise_s bt e "Trying to do x with y" [%sexp { y : Y.t }]
val pp_of_sexp : Sexplib0.Sexp.t -> _ Pp.t

When you need to render a Sexp.t into a _ Pp.t paragraph, things may become tricky - newlines inserted in surprising places, etc. This functions attempts to do an OK job at it.

Hints

val did_you_mean : string -> candidates:string list -> Style.t Pp.t list

Produces a "Did you mean ...?" hint

val am_running_test : unit -> bool

Set by the For_test.wrap when wrapping sections for tests, accessed by libraries if needed.

val error_count : unit -> int

This return the number of errors that have been emitted via Err.error since the last reset_counts. Beware, note that errors raised as exceptions via functions such as Err.raise do not affect the error count. The motivation is to allow exceptions to be caught without impacting the overall exit code.

val had_errors : unit -> bool

A convenient wrapper for Err.error_count () > 0.

This is useful if you are trying not to stop at the first error encountered, but still want to stop the execution at a specific breakpoint after some numbers of errors. To be used in places where you want to stop the flow at a given point rather than returning meaningless data.

val warning_count : unit -> int

Return the number of warnings that have been emitted via Err.warning since the last reset_counts.

Printing messages

val prerr : ?reset_separator:bool -> t -> unit

Print to stderr (not thread safe). By default, prerr will start by writing a blank line on stderr if Err messages have already been emitted during the lifetime of the program. That is a reasonable default to ensure that err messages are always nicely separated by an empty line, to make them more readable. However, if you structure your output manually, perhaps you do not want this. If reset_separator=true, this behavior is turned off, and the first message of this batch will be printed directly without a leading blank line.

Non-raising user errors

This part of the library allows the production of messages that do not raise.

For example: - Emitting multiple errors before terminating - Non fatal Warnings - Debug and Info messages

Errors and warnings are going to affect error_count (and resp. warning_count), which is going to be used by protect to impact the exit code of the application. Use with care.

val error : ?loc:Loc.t -> ?hints:Style.t Pp.t list -> Style.t Pp.t list -> unit

Emit an error on stderr and increase the global error count.

val warning : ?loc:Loc.t -> ?hints:Style.t Pp.t list -> Style.t Pp.t list -> unit

Emit a warning on stderr and increase the global warning count.

val info : ?loc:Loc.t -> ?hints:Style.t Pp.t list -> Style.t Pp.t list -> unit

Emit a information message on stderr. Required verbosity level of Info or more, disabled by default.

val debug : ?loc:Loc.t -> ?hints:Style.t Pp.t list -> Style.t Pp.t list Stdlib.Lazy.t -> unit

The last argument to debug is lazy in order to avoid the allocation when debug messages are disabled. This isn't done with the other functions, because we don't expect other logging functions to be used in a way that impacts the program's performance, and using lazy causes added programming friction.

Handler

To be used by command line handlers, as well as tests.

val protect : ?exn_handler:(exn -> t option) -> (unit -> 'a) -> ('a, int) Stdlib.Result.t

protect f will take care of running f, and catch any user error. If the exit code must be affected it is returned as an Error. This also takes care of catching uncaught exceptions and printing them to the screen. You may provide exn_handler to match on custom exceptions and turn them into Err for display and exit code. Any uncaught exception will be reported as an internal errors with a backtrace. When Err.am_running_test () is true the backtrace is redacted to avoid making expect test traces too brittle. protect starts by performing a reset of the error and warning counts with a call to reset_counts.

module For_test : sig ... end

Private

module Private : sig ... end

Private is used by Err_config. We mean both libraries to work as companion libs. Note any of this can change without notice and without requiring a semver bump, so use at your own risk (or don't).