Expound
Human-optimized error messages for clojure.spec
Install / Use
/learn @bhb/ExpoundREADME
Expound
Expound formats clojure.spec error messages in a way that is optimized for humans to read.
For example, Expound will replace a clojure.spec error message like:
val: {} fails spec: :example/place predicate: (contains? % :city)
val: {} fails spec: :example/place predicate: (contains? % :state)
with
-- Spec failed --------------------
{}
should contain keys: :city, :state
| key | spec |
|========+=========|
| :city | string? |
|--------+---------|
| :state | string? |
Comparison with clojure.spec error messages
Expound is in alpha while clojure.spec is in alpha.
Expound is supported by Clojurists Together. If you find this project useful, please consider making a monthly donation to Clojurists Together (or ask your employer to do so).
Installation
If you are using recent versions of ClojureScript, please check the compatibility guide
Leiningen/Boot
[expound "0.9.0"]
deps.edn
expound/expound {:mvn/version "0.9.0"}
Lumo
npm install @bbrinck/expound
Usage
Quick start via clj
> brew install clojure/tools/clojure
> clojure -Sdeps '{:deps {friendly/friendly {:git/url "https://gist.github.com/bhb/2686b023d074ac052dbc21f12f324f18" :sha "d532662414376900c13bed9c920181651e1efeff"}}}' -X friendly/run
user=> (require '[expound.alpha :as expound])
nil
user=> (expound/expound string? 1)
nil
-- Spec failed --------------------
1
should satisfy
string?
-------------------------
Detected 1 error
user=>
expound
Replace calls to clojure.spec.alpha/explain with expound.alpha/expound and to clojure.spec.alpha/explain-str with expound.alpha/expound-str.
(require '[clojure.spec.alpha :as s])
(require '[expound.alpha :as expound])
(s/def :example.place/city string?)
(s/def :example.place/state string?)
(s/def :example/place (s/keys :req-un [:example.place/city :example.place/state]))
(expound/expound :example/place {:city "Denver", :state :CO} {:print-specs? false})
;; -- Spec failed --------------------
;; {:city ..., :state :CO}
;; ^^^
;; should satisfy
;; string?
;; -------------------------
;; Detected 1 error
*explain-out*
To use other Spec functions, set clojure.spec.alpha/*explain-out* to expound/printer.
(require '[clojure.spec.alpha :as s])
(require '[expound.alpha :as expound])
(s/def :example.place/city string?)
(s/def :example.place/state string?)
;; Use `assert`
(s/check-asserts true) ; enable asserts
;; Set var in the scope of 'binding'
(binding [s/*explain-out* expound/printer]
(s/assert :example.place/city 1))
(set! s/*explain-out* expound/printer)
;; (or alter-var-root - see doc/faq.md)
(s/assert :example.place/city 1)
;; Use `instrument`
(require '[clojure.spec.test.alpha :as st])
(s/fdef pr-loc :args (s/cat :city :example.place/city
:state :example.place/state))
(defn pr-loc [city state]
(str city ", " state))
(st/instrument `pr-loc)
(pr-loc "denver" :CO)
;; You can use `explain` without converting to expound
(s/explain :example.place/city 123)
ClojureScript considerations
Due to the way that macros are expanded in ClojureScript, you'll need to configure Expound in Clojure to use Expound during macro-expansion. This does not apply to self-hosted ClojureScript. Note the -e arg when starting ClojureScript:
clj -Srepro -Sdeps '{:deps {expound {:mvn/version "0.9.0"} org.clojure/test.check {:mvn/version "0.9.0"} org.clojure/clojurescript {:mvn/version "1.10.520"}}}' -e "(require '[expound.alpha :as expound]) (set! clojure.spec.alpha/*explain-out* expound.alpha/printer)" -m cljs.main -re node
As of this commit, ClojureScript instrumentation errors only contain data and leave formatting/printing errors to the edge of the system e.g. the REPL. To format errors in the browser, you must set up some global handler to catch errors and call repl/error->str. For instance, here is a custom formatter for Chrome devtools that uses Expound:
(require '[cljs.repl :as repl])
(require '[clojure.spec.alpha :as s])
(require '[expound.alpha :as expound])
(set! s/*explain-out* expound/printer)
(def devtools-error-formatter
"Uses cljs.repl utilities to format ExceptionInfo objects in Chrome devtools console."
#js{:header
(fn [object _config]
(when (instance? ExceptionInfo object)
(let [message (some->> (repl/error->str object)
(re-find #"[^\n]+"))]
#js["span" message])))
:hasBody (constantly true)
:body (fn [object _config]
#js["div" (repl/error->str object)])})
(defonce _
(some-> js/window.devtoolsFormatters
(.unshift devtools-error-formatter)))
See this ticket for other solutions in the browser.
Printing results for check
Re-binding s/*explain-out* has no effect on the results of clojure.spec.test.alpha/summarize-results, but Expound provides the function expound/explain-results to print the results from clojure.spec.test.alpha/check.
(require '[expound.alpha :as expound]
'[clojure.spec.test.alpha :as st]
'[clojure.spec.alpha :as s]
'[clojure.test.check])
(s/fdef ranged-rand
:args (s/and (s/cat :start int? :end int?)
#(< (:start %) (:end %)))
:ret int?
:fn (s/and #(>= (:ret %) (-> % :args :start))
#(< (:ret %) (-> % :args :end))))
(defn ranged-rand
"Returns random int in range start <= rand < end"
[start end]
(+ start (long (rand (- start end)))))
(set! s/*explain-out* expound/printer)
;; (or alter-var-root - see doc/faq.md)
(expound/explain-results (st/check `ranged-rand))
;;== Checked user/ranged-rand =================
;;
;;-- Function spec failed -----------
;;
;; (user/ranged-rand -3 0)
;;
;;failed spec. Function arguments and return value
;;
;; {:args {:start -3, :end 0}, :ret -5}
;;
;;should satisfy
;;
;; (fn
;; [%]
;; (>= (:ret %) (-> % :args :start)))
Error messages for specs
Adding error messages
If a value fails to satisfy a predicate, Expound will print the name of the function (or <anonymous function> if the function has no name). To improve the error message, you can use expound.alpha/defmsg to add a human-readable error message to the spec.
(s/def :ex/name string?)
(expound/defmsg :ex/name "should be a string")
(expound/expound :ex/name :bob)
;; -- Spec failed --------------------
;;
;; :bob
;;
;; should be a string
You can also reuse messages by defining specs in terms of other specs:
(s/def :ex/string string?)
(expound/defmsg :ex/string "should be a string")
(s/def :ex/city :ex/string)
(expound/expound :ex/city :denver)
;; -- Spec failed --------------------
;;
;; :denver
;; should be a string
Built-in predicates with error messages
Expound provides a default set of type-like predicates with error messages. For example:
(expound/expound :expound.specs/pos-int -1)
;; -- Spec failed --------------------
;;
;; -1
;;
;; should be a positive integer
You can see the full list of available specs with expound.specs/public-specs.
Printer options
expound and expound-str can be configured with options:
(expound/expound :example/place {:city "Denver", :state :CO} {:print-specs? false :theme :figwheel-theme})
or, to configure the global printer:
(set! s/*explain-out* (expound/custom-printer {:show-valid-values? true :print-specs? false :theme :figwheel-theme}))
;; (or alter-var-root - see doc/faq.md)
| name | spec | default | description |
|------|------|----------|-------------|
| :show-valid-values? | boolean? | false | If false, replaces valid values with ... (example below) |
| :value-str-fn | ifn? | provided function | Function to print bad values (example below) |
| :print-specs? | boolean? | true | If true, display "Relevant specs" section. Otherwise, omit that section. |
| :theme | #{:figwheel-theme :none} | :none | Enables color theme. |
:show-valid-values?
By default, printer will omit valid values and replace them with ...
(set! s/*explain-out* expound/printer)
;; (or alter-var-root - see doc/faq.md)
(s/explain :example/place {:city "Denver" :state :CO :country "USA"})
;; -- Spec failed --------------------
;;
;; {:city ..., :state :CO, :country ...}
;; ^^^
;;
;; should satisfy
;;
;; string?
You can configure Expound to show valid values:
(set! s/*explain-out* (expound/custom-printer {:show-valid-values? true}))
;; (or alter-var-root - see doc/faq.md)
(s/explain :example/place {:city "Denver" :state :CO :country "USA"})
;; -- Spec failed --------------------
;;
;; {:city "Denver", :state :CO, :country "USA"}
;; ^^^
;;
;; should satisfy
;;
;; string?
:value-str-fn
You can provide your own function to display the invalid value.
;; Your implementation should meet the following spec:
(s/fdef my-value-str
:args (s/cat
:spec-name (s/nilable #{:args :fn :ret})
:form any?
Related Skills
product-manager-skills
31PM skill for Claude Code, Codex, Cursor, and Windsurf: diagnose SaaS metrics, critique PRDs, plan roadmaps, run discovery, and coach PM career transitions.
devplan-mcp-server
3MCP server for generating development plans, project roadmaps, and task breakdowns for Claude Code. Turn project ideas into paint-by-numbers implementation plans.
