# Expound rewrites clojure.spec failures as tables and carets

> Expound is a small Clojure library that reformats clojure.spec errors so a person can read them. It replaces the raw predicate lines with a printed value, a table of expected specs and a caret under the offending part, and it needs different wiring for Clojure and for ClojureScript.

**bhb/expound** — Human-optimized error messages for clojure.spec

- Repository: https://github.com/bhb/expound
- Stars: 935 · Forks: 20
- Language: Clojure
- License: EPL-1.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/bhb-expound

## The raw output names the predicate but not the problem

clojure.spec reports failures as flat lines that name a value and a predicate. The project shows the starting point for a map that should carry two keys:

```clojure
val: {} fails spec: :example/place predicate: (contains? % :city)
val: {} fails spec: :example/place predicate: (contains? % :state)
```

Each line repeats the value, the spec keyword and the predicate, so a reader has to work out which key is missing and what type was expected. That output is written for something that reads a log, and it costs a developer time every time a spec fails in a REPL or a test run. Expound exists to replace that stream with a report meant to be read by a person, without changing the specs themselves. It is a Clojure library published for both Clojure and ClojureScript, and the project states plainly that Expound is in alpha while clojure.spec is in alpha, which sets expectations for how settled the formatting is.

## A printed value, a key table and a caret under the failure

Expound replaces two functions. expound.alpha/expound stands in for clojure.spec.alpha/explain, and expound.alpha/expound-str stands in for explain-str. Given the same map that produced the two predicate lines, the formatted output is:

```
-- Spec failed --------------------

  {}

should contain keys: :city, :state

| key    | spec    |
|========+=========|
| :city  | string? |
|--------+---------|
| :state | string? |
```

The value is printed once, the missing keys are collected into one sentence, and a table maps each key to the spec that governs it. For a scalar the shape is different again: the value, then the phrase should satisfy, then the predicate itself. Where a single position inside a value is at fault, a caret is placed under it. The point of the redesign is that the offending value and the expected spec appear in one glance, instead of being spread across separate failure lines.

## Installing version 0.9.0 across four toolchains

Expound ships as a Clojure library and as an npm package, and the coordinates for each toolchain are given side by side. For Leiningen or Boot the dependency is:

```clojure
[expound "0.9.0"]
```

For a deps.edn project the coordinate is expressed as a map:

```clojure
expound/expound {:mvn/version "0.9.0"}
```

For Lumo, the npm route installs the ClojureScript build:

```bash
npm install @bbrinck/expound
```

The npm package is published as @bbrinck/expound at version 0.9.0, with the same one line description and the same EPL-1.0 licence identifier as the Clojure artifact. A warning sits above the install section, telling users of recent ClojureScript versions to check the compatibility guide in doc/compatibility.md before pinning a version. The version number in the Clojars badge, in the deps.edn snippet and in package.json is the same 0.9.0, so the three routes are meant to land on the same code.

## One dynamic var redirects every other spec printer

The cleanest integration point is a dynamic var. Setting clojure.spec.alpha/*explain-out* to expound/printer redirects every other Spec function, not just the two you call yourself:

```clojure
(binding [s/*explain-out* expound/printer]
  (s/assert :example.place/city 1))
```

The global form uses set! on the var, and the project mentions alter-var-root as an alternative with a pointer to doc/faq.md. That matters because s/assert is the path most Clojure code takes, and the same rebinding also covers s/instrument, which applies specs to a function at runtime:

```clojure
(st/instrument `pr-loc)
(pr-loc "denver" :CO)
```

The documentation is explicit that s/explain can still be called directly while the printer is bound. The rebinding is therefore additive rather than a replacement, so a codebase can keep its existing explain calls and change only the printer.

## ClojureScript needs the setup performed in Clojure

This is where the library gets awkward, and the reason is macro expansion. Because ClojureScript expands macros on the Clojure side, Expound has to be configured in Clojure for the formatting to appear during macro expansion. The full command includes the -e argument that performs the setup before the compiler starts:

```bash
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
```

Self-hosted ClojureScript is exempt from that requirement. The second half of the problem is the browser: newer ClojureScript instrumentation errors carry data only, so formatting has to happen at the edge of the system. The project's answer is a Chrome devtools formatter that calls repl/error->str and pushes itself onto js/window.devtoolsFormatters, with a link to issue 152 for other approaches. None of that is a one line configuration.

## check results do not follow the printer rebinding

There is one gap in the mechanism that catches people out. Re-binding s/*explain-out* has no effect on the results of clojure.spec.test.alpha/summarize-results, so the test checker keeps producing its own report even with the printer swapped. Expound covers that case with a separate function, expound/explain-results, which takes the result of clojure.spec.test.alpha/check:

```clojure
(expound/explain-results (st/check `ranged-rand))
```

The printed result follows the same style as the rest of the library, with a checked function header, the failing argument list and return value, and the predicate that was not satisfied. The practical consequence is that a test suite has two paths to configure: the printer for asserts and instrumented calls, and this extra call wherever check results are printed. A team migrating a large suite has to touch each place that reports check results, not only the shared setup file.

## Alpha status, no tagged releases, EPL-1.0

The maintenance picture here is modest. The repository has no GitHub releases, so there is no tag to pin in that sense and no release list to scan, although a CHANGELOG.md file sits at the top level. The current version lives in package.json and in the installation snippets as 0.9.0. The last push to the master branch was on 2026-10-20, so the code in the tree reflects work done within the last few days. The licence field on the repository says EPL-1.0, the Eclipse Public Licence, a file level copyleft licence that is common in the Clojure ecosystem. Teams whose policy treats copyleft dependencies as a special case should read EPL-1.0 before vendoring it into a product, while teams already shipping Clojure code will usually find nothing unusual in it. Funding goes through Clojurists Together, which is a detail about maintenance cost rather than about the API.

## Conclusion

Expound suits Clojure and ClojureScript teams whose spec failures are read by people in a REPL or a test report, and it costs one dependency plus a printer rebinding to try. It is the wrong tool for a codebase that only inspects spec output in a log, and it will not fix specs that fail for reasons the message cannot explain. Verify first that your ClojureScript version is on the compatibility path described in doc/compatibility.md, that the expound version you pin matches the Clojars and npm releases, and that any check results you print go through expound/explain-results.

## FAQ

### How do I install Expound in a deps.edn project?

Add the coordinate expound/expound with the mvn version set to 0.9.0. Leiningen and Boot projects use the vector form [expound "0.9.0"] instead, and Lumo users install the npm package @bbrinck/expound.

### Does Expound work with ClojureScript?

Yes, with extra setup. Because ClojureScript expands macros on the Clojure side, Expound must be configured in Clojure during macro expansion, except under self-hosted ClojureScript. In the browser you also need a global handler that calls repl/error->str, since newer ClojureScript instrumentation errors contain data only.

### How do I format the output of clojure.spec.test.alpha/check with Expound?

Rebinding s/*explain-out* does not change summarize-results, so the check report stays in the default format. Use expound/explain-results on the result of clojure.spec.test.alpha/check to get the Expound layout instead.

### What licence does Expound use?

The licence field on the repository says EPL-1.0, and the npm package @bbrinck/expound declares the same identifier. Both the Clojure and ClojureScript artifacts ship under those terms.

## Sources

- [bhb/expound on GitHub](https://github.com/bhb/expound)
- [Issues](https://github.com/bhb/expound/issues)
- [License: EPL-1.0](https://github.com/bhb/expound/blob/master/LICENSE)
- [README](https://github.com/bhb/expound/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bhb-expound
