Mavryk_error_monad.Error_monadMavryk Protocol Implementation - Error Monad
type error_category = [ | `BranchErrors that may not happen in another context
*)| `TemporaryErrors that may not happen in a later context
*)| `PermanentErrors that will happen no matter the context
*) ]Categories of error
Note: this is only meaningful within the protocol. It may be removed from the error monad and pushed to the protocol environment in the future. See https://gitlab.com/tezos/tezos/-/issues/1576
type error = TzCore.error = ..The main error type.
Whenever you add a variant to this type (with type Error_monad.error += …) you must also register the error with register_error_kind.
These errors are not meant to be inspected in general. Meaning that they should not be matched upon. Consequently it is acceptable to register an error in an implementation file and not mention it in the corresponding interface file.
CORE: encoding and pretty-printing for errors
include Sig.CORE
with type error := error
and type error_category := error_categoryval string_of_category : error_category -> stringval error_encoding : error Data_encoding.tThe encoding for errors.
Note that this encoding has a few peculiarities, some of which may impact your code. These peculiarities are due to the type error being an extensible variant.
Because the error type is an extensible variant, you must register an encoding for each error constructor you add to error. This is done via register_error_kind.
Because the error type is an extensible variant, with dynamically registered errors (see peculiarity above), there are no tags associated with each error. This does not affect the JSON encoding, but it does impose restrictions on the binary encoding. The chosen workaround is to encode errors as JSON and to encode the JSON to binary form. As a result, errors can be somewhat large in binary: they include field names and such.
Because the error type is an extensible variant, with dynamically registered errors (see peculiarity above), the encoding must be recomputed when a new error is registered. This is achieved by the means of a Data_encoding.delayed combinator: the encoding is recomputed on-demand. There is a caching mechanism so that, in the case where no new errors have been registered since the last use, the last result is used.
This last peculiarity imposes a limit on the use of error_encoding itself. Specifically, it is invalid to use error_encoding inside the ~json argument of a Data_encoding.splitted. This is because splitted evaluates the delayed combinator once-and-for-all to produce a json encoding. (Note that the following data-encoding combinators use splitted internally: Data_encoding.Compact.make, Data_encoding.assoc, and Data_encoding.lazy_encoding. As a result, it is invalid to use error_encoding within the arguments of these combinators as well.)
val pp : Stdlib.Format.formatter -> error -> unitval register_error_kind :
error_category ->
id:string ->
title:string ->
description:string ->
?pp:(Stdlib.Format.formatter -> 'err -> unit) ->
'err Data_encoding.t ->
(error -> 'err option) ->
('err -> error) ->
unitThe error data type is extensible. Each module can register specialized error serializers id unique name of this error. Ex.: overflow_time_counter title more readable name. Ex.: Overflow of time counter description human readable description. Ex.: The time counter overflowed while computing delta increase pp formatter used to pretty print additional arguments. Ex.: The time counter overflowed while computing delta increase. Previous value %d. Delta: %d encoder decoder data encoding for this error. If the error has no value, specify Data_encoding.empty
val classify_error : error -> Error_classification.tClassify an error using the registered kinds
Catch all error when 'serializing' an error.
Catch all error when 'deserializing' an error.
val json_of_error : error -> Data_encoding.jsonAn error serializer
val error_of_json : Data_encoding.json -> errortype error_info = {category : error_category;id : string;title : string;description : string;schema : Data_encoding.json_schema;}Error information
val pp_info : Stdlib.Format.formatter -> error_info -> unitval find_info_of_error : error -> error_infofind_info_of_error e retrieves the `error_info` associated with the given error `e`.
val get_registered_errors : unit -> error_info listRetrieves information of registered errors
WITH_WRAPPED: wrapping of errors from other instantiations within this one. Specifically, this is used to wrap errors of the economic protocol (e.g., operation is invalid) within the errors of the shell (e.g., failed to validate protocol data).
Functions from this module should only be used within the environment.
include Sig.WITH_WRAPPED with type error := errormodule type Wrapped_error_monad = sig ... endThe purpose of this module is to wrap a specific error monad E into a more general error monad Eg.
val register_wrapped_error_kind :
(module Wrapped_error_monad) ->
id:string ->
title:string ->
description:string ->
unitSame as register_error_kind but for a wrapped error monad. The codec is defined in the module parameter. It makes the category of the error Wrapped instead of Main.
TzTrace: trace module specific to the Mavryk Error monad. The trace type of this module is meant to become abstract in the medium-term (see https://gitlab.com/tezos/tezos/-/issues/1577).
type 'error trace = 'error TzTrace.traceTzMonad: the Mavryk-specific monad part of the Error_monad. It includes
include Monad_maker.S
with type error := TzCore.error
and type 'error trace := 'error TzTrace.tracetype tztrace = TzCore.error TzTrace.tracetype 'a tzresult = ('a, tztrace) Stdlib.resultmodule Lwt_syntax : module type of TzLwtreslib.Monad.Lwt_syntaxmodule Result_syntax : sig ... endmodule Lwt_result_syntax : sig ... endval classify_trace : tztrace -> Error_classification.tval pp_print_trace : Stdlib.Format.formatter -> tztrace -> unitval pp_print_top_error_of_trace : Stdlib.Format.formatter -> tztrace -> unitPretty-prints the top error of a trace
val trace_encoding : tztrace Data_encoding.tval result_encoding : 'a Data_encoding.t -> 'a tzresult Data_encoding.tA serializer for result of a given type
val record_trace :
'err ->
('a, 'err TzTrace.trace) Stdlib.result ->
('a, 'err TzTrace.trace) Stdlib.resultrecord_trace err res is either res if res is Ok _, or it is Error (Trace.cons err tr) if res is Error tr.
In other words, record_trace err res enriches the trace that is carried by res (if it is carrying a trace) with the error err. It leaves res untouched if res is not carrying a trace.
You can use this to add high-level information to potential low-level errors. E.g.,
record_trace
Failure_to_load_config
(load_data_from_file config_encoding config_file_name)Note that record_trace takes a fully evaluated error err as argument. It means that, whatever the value of the result res, the error err is evaluated. This is not an issue if the error is a simple expression (a literal or a constructor with simple parameters). However, for any expression that is more complex (e.g., one that calls a function) you should prefer record_trace_eval.
val trace :
'err ->
('b, 'err TzTrace.trace) Stdlib.result Lwt.t ->
('b, 'err TzTrace.trace) Stdlib.result Lwt.ttrace is identical to record_trace but applies to a promise. More formally, trace err p is a promise that resolves to Ok v if p resolves to Ok v, or it resolves to Error (Trace.cons err tr) if res resolves to Error tr.
In other words, trace err p enriches the trace that p resolves to (if it does resolve to a trace) with the error err. It leaves the value that p resolves to untouched if it is not a trace.
You can use this to add high-level information to potential low-level errors.
Note that, like record_trace, trace takes a fully evaluated error as argument. For a similar reason as explained there, you should only use trace with simple expressions (literal or constructor with simple parameters) and prefer trace_eval for any other expression (such as ones that include functions calls).
val record_trace_eval :
(unit -> 'err) ->
('a, 'err TzTrace.trace) Stdlib.result ->
('a, 'err TzTrace.trace) Stdlib.resultrecord_trace_eval is identical to record_trace except that the error that enriches the trace is wrapped in a function that is evaluated only if it is needed. More formally record_trace_eval mkerr res is res if res is Ok _, or it is Error (Trace.cons (mkerr ()) tr) if res is Error tr.
You can achieve the same effect by hand with
match res with
| Ok _ -> res
| Error tr -> Error (Trace.cons (mkerr ()) tr)Prefer record_trace_eval over record_trace when the enriching error is expensive to compute or heavy to allocate.
val trace_eval :
(unit -> 'err) ->
('b, 'err TzTrace.trace) Stdlib.result Lwt.t ->
('b, 'err TzTrace.trace) Stdlib.result Lwt.ttrace_eval is identical to trace except that the error that enriches the trace is wrapped in a function that is evaluated only if and when it is needed. More formally trace_eval mkerr p is a promise that resolves to Ok v if p resolves to Ok v, or it resolves to Error (Trace.cons err tr) if p resolves to Error tr and then mkerr
() resolves to err.
You can achieve the same effect by hand with
p >>= function
| Ok _ -> p
| Error tr ->
mkerr () >>= fun err ->
Lwt.return (Error (Trace.cons err tr))Note that the evaluation of the error can be arbitrarily delayed. Avoid using references and other mutable values in the function mkerr.
Prefer trace_eval over trace when the enriching error is expensive to compute or heavy to allocate or when evaluating it requires the use of Lwt.
val error_unless : bool -> 'err -> (unit, 'err TzTrace.trace) Stdlib.resulterror_unless flag err is Ok () if b is true, it is Error (Trace.make err) otherwise.
val error_when : bool -> 'err -> (unit, 'err TzTrace.trace) Stdlib.resulterror_when flag err is Error (Trace.make err) if b is true, it is Ok () otherwise.
val fail_unless :
bool ->
'err ->
(unit, 'err TzTrace.trace) Stdlib.result Lwt.tfail_unless flag err is Lwt.return @@ Ok () if b is true, it is Lwt.return @@ Error (Trace.make err) otherwise.
val fail_when : bool -> 'err -> (unit, 'err TzTrace.trace) Stdlib.result Lwt.tfail_when flag err is Lwt.return @@ Error (Trace.make err) if b is true, it is Lwt.return @@ Ok () otherwise.
val unless :
bool ->
(unit -> (unit, 'trace) Stdlib.result Lwt.t) ->
(unit, 'trace) Stdlib.result Lwt.tunless b f is f () if b is false and it is a promise already resolved to Ok () otherwise.
You can use unless to avoid having to write an if statement that you then need to populate entirely to satisfy the type-checker. E.g, you can write unless b f instead of if not b then f () else return_unit.
val when_ :
bool ->
(unit -> (unit, 'trace) Stdlib.result Lwt.t) ->
(unit, 'trace) Stdlib.result Lwt.twhen_ b f is f () if b is true and it is a promise already resolved to Ok () otherwise.
You can use when_ to avoid having to write an if statement that you then need to populate entirely to satisfy the type-checker. E.g, you can write when_ b f instead of if b then f () else return_unit.
module Option_syntax = TzLwtreslib.Monad.Option_syntaxmodule Lwt_option_syntax = TzLwtreslib.Monad.Lwt_option_syntaxThis part of the interface groups functions that are used to interact with code that raises exceptions. Typically, you should be using these functions when calling into a library that raises exceptions.
Remember that the keyword error is for failure within the Result monad (or, more specifically, the TracedResult monad) whilst fail is for failure within the LwtResult monad (or, more specifically, the LwtTracedResult monad).
This sub-part of the interface groups functions that fail (either in the TracedResult monad or the LwtTracedResult monad) whilst carrying information provided as argument. When reading this sub-part you should read error and fail as verbs. E.g., error_with_exn errors out and carries a provided exception. The next sub-part will group noun-like, declarative functions.
val error_with :
('a, Stdlib.Format.formatter, unit, 'b tzresult) Stdlib.format4 ->
'aerror_with fmt … errors out: it fails within the TracedResult monad. The payload of the Error constructor is unspecified beyond the fact that it includes the string formatted by fmt …. E.g.,
if n < 0 then
error_with "Index (%d) is negative" n
else if n >= Array.length a then
error_with "Index (%d) is beyond maximum index (%d)" n (Array.length a - 1)
else
Ok a.(n)Note: this is somewhat equivalent to Stdlib.failwith in that it is a generic failure mechanism with a simple error message. Like Stdlib.failwith it should be replaced by a more specific error mechanism in most cases.
val failwith :
('a, Stdlib.Format.formatter, unit, 'b tzresult Lwt.t) Stdlib.format4 ->
'afailwith fmt … fails: it fails within the LwtTracedResult monad. The payload of the Error constructor is unspecified beyond the fact that it includes the string formatted by fmt …. E.g.,
match find key store with
| None ->
failwith "Key %a not found in store" pp_key key
| Some value ->
LwtResult.return valueNote: this is somewhat equivalent to Stdlib.failwith in that it is a generic failure mechanism with a simple error message. Like Stdlib.failwith it should be replaced by a more specific error mechanism in most cases.
val error_with_exn : exn -> 'a tzresulterror_with_exn exc errors out: it fails within the TracedResult monad. The payload of the Error constructor is unspecified but it includes the exception.
It is meant as a way to switch from exception-based error management to tzresult-based error management, e.g., when calling external libraries that use exceptions.
try Ok (parse_input s) with Lex_error | Parse_error as exc -> error_with_exn excWhilst it is useful in specific places, it is generally better to use a dedicated error.
val fail_with_exn : exn -> 'a tzresult Lwt.tfail_with_exn exc fails: it fails within the LwtTracedResult monad. The payload of the Error constructor is unspecified but it includes the info from the exception.
It is meant as a way to switch, inside of Lwt code, from exception-based error management to tzresult-based error management, e.g., when calling external libraries that use exceptions.
Lwt.catch
(fun () -> parse_input s)
(function
| Lex_error | Parse_error as exc -> fail_with_exn exc
| exn -> Lwt.reraise exn (* re-raise by default *))Whilst it is useful in specific places, it is generally better to use a dedicated error.
This sub-part of the interface groups declarative functions that convert between different styles of error (exceptions, errors, traces, results). By themselves these functions have no effect within the Result or LwtResult monad, and they are generally used along with constructors or combinators.
val error_of_exn : exn -> errorerror_of_exn e is an error that carries the exception e. This function is intended to be used when interacting with a part of the code (most likely an external library) which uses exceptions.
val error_of_fmt :
('a, Stdlib.Format.formatter, unit, error) Stdlib.format4 ->
'aerror_of_fmt … is like error_with … but the error isn't wrapped in a trace in a result. Instead, an error is returned and the caller is expected to pass it to whichever error-combinator is appropriate to the situation. E.g.,
fail_unless (check_valid input) (error_of_fmt "Invalid_input: %a" pp input)Wrapped OCaml/Lwt exception
Cancelation
val protect :
?on_error:(error trace -> 'a tzresult Lwt.t) ->
?canceler:Lwt_canceler.t ->
(unit -> 'a tzresult Lwt.t) ->
'a tzresult Lwt.tprotect is a wrapper around Lwt.catch where the error handler operates over trace instead of exn. Besides, protect ~on_error ~canceler ~f may *cancel* f via a Lwt_canceler.t.
More precisely, protect ~on_error ~canceler f runs f (). An Lwt failure triggered by f () is wrapped into an Exn. If a canceler is given and Lwt_canceler.cancellation canceler is determined before f (), a Canceled error is returned.
Errors are caught by ~on_error (if given), otherwise the previous value is returned. An Lwt failure triggered by ~on_error is wrapped into an Exn
val catch : ?catch_only:(exn -> bool) -> (unit -> 'a) -> 'a tzresultcatch f executes f within a try-with block and wraps exceptions within a tzresult. catch f is equivalent to try Ok (f ()) with e -> Error (error_of_exn e).
If catch_only is set, then only exceptions e such that catch_only e is true are caught.
Whether catch_only is set or not, this function never catches non-deterministic runtime exceptions of OCaml such as Stack_overflow and Out_of_memory.
catch_f f handler is equivalent to map_error (catch f) handler. In other words, it catches exceptions in f () and either returns the value in an Ok or passes the exception to handler for the Error.
No attempt is made to catch the exceptions raised by handler.
catch_only has the same use as with catch. The same restriction on catching non-deterministic runtime exceptions applies.
val catch_s :
?catch_only:(exn -> bool) ->
(unit -> 'a Lwt.t) ->
'a tzresult Lwt.tval protect_result :
?canceler:Lwt_canceler.t ->
(unit -> 'a Lwt.t) ->
('a, exn) Stdlib.result Lwt.tprotect_result is similar to protect except that any non runtime exception raised by Lwt.catch is wrapped under an Error value.