recide, second-person singular present imperative of recidere
: - to fall back, come to naught; to curtail
ErrorForm
Clojure's ex-info
is a very useful construct: you can attach a map of arbitrary data to a thrown exception, allowing code that has caught the exception to examine, log, or otherwise process this ex-data
.
For example, assert
statements provide valuable sanity checks in business logic, but when they fail it is usually highly desirable to know how they failed. One could attempt to insert this information into the exception string, but sometimes the relevant data is much too large for a wieldy exception message. Instead checking for the desired property and throwing an ex-info
with all the relevant data attached can preserve the succinctness of error messages while saving the developer enormous amounts of time, especially when debugging at the REPL.
One of the principal weaknesses of ex-info
is that its use can encourage ad-hoc exceptions with no standards: if you catch an ex-info, its type is the same as all other ex-infos, its string is arbitrary, and not only are you unable to count on any particular key appearing in the ex-data
, it's quite possible that the map is entirely empty.
If you want to enjoy the benefits of using ex-info
widely in a large project, but you also want to retain ~~a measure of sanity~~ the benefits of well-defined errors, it is likely that you will eventually resort, in each logical component of your application, either to breaking with universal Clojure idioms or to defining your own defining a standard set of "throwing functions" which use ex-info
but guarantee a certain rigidity: perhaps a common prefix to the exception strings, perhaps certain guaranteed keys in the ex-data
maps.
The primary purpose of this library, recide, is to provide tools to ease this process. It provides utilities for defining standard ex-info forms, as well as the capacity for checking at compile-time that they are being used as intended.
All ex-info
maps generated by tools in recide
contain at least two keys:
:recide/error
, whose value is an instance of ErrorForm
.ErrorForm
(default in recide is :recide/type
).recide.core/insist
is analogous to assert
. Its signature is the same, and just like assert
it only executes when clojure.core/*assert*
is true.
But instead of throwing an AssertionError
, it throws an ex-info
with an explanatory string of the form: "Assertion failed: <asserted expression or supplied message>". The type in the ex-data is :recide/assertion
. There are two other keys used by insist
:
:expression
, whose value is the actual expression contained in the insist
:values
, whose value is a map from each variable used in the expression to its value at the time of failure.:values
is only present whenever recide.impl/*capture-insists*
is true, but it is false by default. At library loading time, it is set to true if at least one of the following is true:
The signature of insist
is [[expr] [expr message]]
, and the ex-data in the resulting ex-info has the following form:
{:recide/type :recide/assertion,
:expression <expr>
:values { ... }}
Example in use:
(let [y not
x true]
(insist (y x)))
;; Unhandled clojure.lang.ExceptionInfo
;; Assertion failed: (y x)
;; {:expression (y x),
;; :values {y #function[clojure.core/not],
;; x true},
;; :recide/type :recide/assertion}
recide.core/error
has two arities: ([type msg data] [type msg data cause])
. error
constructs an ex-info
with the map data
attached, whose type (again, signified by :recide/type
by default) is type
. Supplying a cause
merely gives the exception a cause according to Java idiom.
(let [x "not b! haha"]
(raise :my-type
"my explanation!"
{:a "a"
:b x}))
;; #error {
;; :cause "my explanation!"
;; :data {:a "a",
;; :b "not b! haha",
;; :recide/type :my-type, :recide/error #object[...]}
;; :via
;; [{:type clojure.lang.ExceptionInfo
;; :message "my explanation!"
;; :data {:a "a",
;; :b "not b! haha",
;; :recide/type :my-type, :recide/error #object[...]}
;; :at [clojure.core$ex_info invokeStatic "core.clj" 4725]}]
;; :trace ... }
recide.core/raise
has the same two arities. raise
throws the exception constructed by error
.
assoc
/ update
into recide errors' ex-dataConvenience functions: recide.core/assoc-error
and recide.core/update-error
each return new exceptions of the original type, with the ex-data modified as with assoc
and update
.
Sometimes it can be convenient to pass around and manipulate a map representation of an error before turning it back into an exception and throwing it. To this end, we provide recide.core/error->map
and recide.core/error-map->throwable
which do as you would expect.
Additionally, we provide an alternative error-map
"constructor" for errors that returns the map form, and the corresponding predicate error-map?
.
For any error of type type
with message msg
, ex-data data
, and cause cause
, the persistent map looks like this:
{:recide/error <ErrorForm>,
:recide/type type,
:recide/msg msg,
:recide/cause cause,
:recide/data data}
All these keys except :recide/error
can be modified by supplying a customized ErrorForm
(see below for details).
raise
and error
do very little to provide standard exception types. To address this further, recide provides deferror
and deferror-group
.
deferror
deferror
is a macro which takes an error-name, a type, and a "generic" string that will prefix the messages of all errors of this type. It also, optionally, takes a collection of required keys. If required keys are specified, then compile-time errors will be thrown any time the tools defined by deferror
are used without specifying those keys explicitly in the source code.
Example usage:
(deferror storage-timeout
:storage/timeout
"A storage operation timed out"
[:method-at-fault :timeout-ms])
In this example, this call to deferror
will define two new macros, storage-timeout
and raise-storage-timeout
. For your convenience, any competent IDE will be able to access detailed docstrings on these new vars:
> (clojure.repl/doc storage-timeout)
;; -------------------------
;; my-ns/storage-timeout
;; [[detail-str data] [detail-str data cause]]
;; Macro
;; Records this raise-site under :storage/timeout in recide, and expands into the equivalent of:
;;
;; (ex-info (str "A storage operation timed out: " detail-str)
;; (assoc data :recide/type :storage/timeout)
;; cause)
;;
;; The following keys are required in the data-map:
;; #{:method-at-fault,
;; :timeout-ms}
> (clojure.repl/doc raise-storage-timeout)
;; -------------------------
;; [[detail-str data] [detail-str data cause]]
;; Macro
;; Records this raise-site under :storage/timeout in recide, and expands into:
;;
;; (raise :storage/timeout
;; (str "A storage operation timed out: " detail-str)
;; data
;; cause)
;;
;; The following keys are required in the data-map:
;; #{:method-at-fault,
;; :timeout-ms}
If you attempt to use either of these without, in your data map, specifying each of the required keys, the Clojure compiler will throw an exception:
> (raise-storage-timeout "blah" {:method-at-fault 'not-really-a-method})
;; Unhandled clojure.lang.ExceptionInfo
;; Assertion failed: storage-timeout requires the following missing
;; keys in its data: :timeout-ms
deferror-group
deferror-group
is a macro which defines a whole family of errors. It takes an error-name, a declaration of base type, and some number of subtype declarations.
Each base type declaration must be either a keyword representing the common namespace for this group of errors, or a tuple whose first element is such a keyword and whose second element is a sequence of required keys. Keys specified here will be required in every subtype.
Each subtype declaration consists of a sequence whose first term is a symbol, second term is a generic string for the error, and third (optional) term is a sequence of required keys for that subtype.
Example:
(deferror-group parse-err
(:query.invalid [:expression])
(find-spec "Invalid find spec")
(inputs "Invalid inputs" [:invalid]))
In this example, there are two error types defined: :query.invalid/find-spec
and :query.invalid/inputs
. The first requires :expression
in its data map, but the second requires both :expression
and :invalid
.
As with deferror
, the utilities produced by deferror-group
have detailed docstrings:
> (clojure.repl/doc parse-err)
;; -------------------------
;; recide/parse-err
;; [[subtype detail-str data] [subtype detail-str data cause]]
;; Macro
;; Records this raise-site under :query.invalid/<subtype> in recide, and expands into the
;; equivalent of:
;;
;; (ex-info (str "<subtype-generic-str>: " detail-str)
;; (assoc data
;; :recide/type
;; :query.invalid/<subtype>)
;; cause)
;;
;; The following map shows, for each subtype, what keywords are required in
;; the data map, and what the generic portion of the string will be:
;;
;; {:find-spec {:required #{:expression},
;; :generic-str "Invalid find spec"},
;; :inputs {:required #{:expression :invalid},
;; :generic-str "Invalid inputs"}}
> (clojure.repl/doc raise-parse-err)
;; -------------------------
;; recide/raise-parse-err
;; [[subtype detail-str data] [subtype detail-str data cause]]
;; Macro
;; Records this raise-site under :query.invalid/<subtype> in recide, and expands into:
;;
;; (raise :query.invalid/<subtype>
;; (str "<subtype-generic-str>: " detail-str)
;; data
;; cause)
;;
;; The following map shows, for each subtype, what keywords are required in
;; the data map, and what the generic portion of the string will be:
;;
;; {:find-spec {:required #{:expression},
;; :generic-str "Invalid find spec"},
;; :inputs {:required #{:expression :invalid},
;; :generic-str "Invalid inputs"}}
As seen before, required keys generate compile-time errors when omitted.
> (raise-parse-err :inputs "detailed this, detailed that" {:expression nil})
;; Unhandled clojure.lang.ExceptionInfo
;; Assertion failed: parse-err called with subtype :inputs requires
;; the following missing keys in its data: :invalid
If we are using keywords to designate error types, it would be useful to be able to catch
errors by means of these keywords. Recide provides try*
for this purpose. try*
is a macro that expands to Clojure's try
, which is one of Clojure's few special forms. In most cases, try*
then should behave exactly like try
. It differs in that it exposes enhanced catch
functionality. You can catch:
instance?
check.ErrorForm
used to construct them)recide.core/try*
[(try* expr* catch-clause* finally-clause?)]
Macro
Expands to Clojure's try Special Form, allowing for enhanced `catch` clauses:
You can catch:
* Classes/Interfaces (represents an instance? check)
`(catch RuntimeException e ...)`
* keywords (recide error types; fully-qualified: :namspace/name, wildcard: :namespace/*)
`(catch :library/error e ...)`
* arbitrary predicates
`(catch bad-error? e ...)`
You can also catch conjunctions/disjunctions of these:
* conjunction
`(catch :and [RuntimeException :library/error bad-error?] e ...)`
* disjunction
`(catch :or [IllegalArgumentException :library/error bad-error?] e ...)`
You can also negate each of these:
`(catch (:not RuntimeException) e ...)`
`(catch :and [(:not RuntimeException) :library/*] e ...)`
Otherwise, behavior should match 'normal' catch clauses in `clojure.core/try`.
Note that you can use keywords of the form :namespace/*
as wildcards to catch families of recide errors, such as those defined by deferror-group
.
> (try* (raise :genus/species-1
"went extinct"
{:year -1839421})
(catch :genus/* e
(println (:year (ex-data e)))))
;; -1839421
ErrorForm
By default, errors raised by this library use ex-info
as a constructor, recide.utils/serialize-throwable
and recide.utils/deserialize-throwable
for (de)serialization, and in map form they use :recide/type
, :recide/msg
, :recide/data
, and :recide/cause
as their standard keywords.
By defining a new ErrorForm
, you can change all of this behavior for your own library. By modifying the keywords, you can "brand" errors coming out of your library. You can swap out ex-info
for another constructor of the same arity, which will be expected to return an IExceptionInfo
. For instance, concerns related to Java interop might motivate creating a new exception class, while Clojure idioms might motivate retaining ex-info compatibility.
You can easily define your customization with recide.core/def-error-form
, overriding only the methods you wish to override; methods not specified default to recide library defaults.
The error-handling methods in recide.core
are agnostic to the specific ErrorForm
used. To create errors using a custom ErrorForm
, you can easily generate a full suite of recide methods tailored specifically to your customizations with recide.core/generate-library!
.
(ns my-library.error
(:require [recide.core :as rc]))
(rc/def-error-form custom-error-form
(type-kw [_] :my-library/type)
(constructor [_] my-library/error-constructor)
;; all other methods are filled out with recide defaults.
(def ^:dynamic *capture-insists?* true)
(rc/generate-library! custom-error-form *capture-insists?*)
;; recide.core/generate-library!
;; [custom-error capture-flag]
;; Macro
;; Generates and defs custom versions of the following recide.core methods, tailored specifically
;; to custom-error, with variable capture in the generated insist subject to capture-flag.
;; * error
;; * error?
;; * error-map
;; * error-map?
;; * throwable->error-map
;; * raise
;; * insist
;; * deferror
;; * deferror-group
;;
;; custom-error should be an instance of recide.error/ErrorForm (see def-error-form).
;; capture-flag must be resolvable to a dynamic var.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close