Clojure Time Bomb 💣

Recently we ran into an issue in our Nextjournal Clojure codebase which was hard to reproduce and it took a while to see what was going on. Let me show what we found:

The symptom was a "clojure.lang.Var$Unbound cannot be cast to .." error which only happened in our CI or when we restarted the Clojure app.

It turned out, we were using a declare’d var inside a def which blows up at some point later even if the var has def’d by then. A trivial example:

(declare bomb)
(def box {:mybomb bomb})
(def bomb "bomb supposedly disarmed")
(clojure.string/blank? (get box :mybomb))
0.4s
Clojure

While this is reasonable, the error message "clojure.lang.Var$Unbound cannot be cast to .." is not necessarily the most helpful one...

What is happening?

The Clojure compiler reads and evaluates a Clojure file one top level form at a time. So at the time bomb is used, but not yet def’d (unbound), the box map contains a value representing the then unbound var.

(pr box)
0.7s
Clojure

Defining the var later doesn’t change it in box.

In a REPL-driven workflow, one would run into this error only once. This is because when bomb has been defined once, the error will not occur again until the Clojure process is restarted. The error goes away when just re-running the code cell above. This made it a little bit tricky to understand what was going on.

Lesson Learned

Don’t use a declare’d var within a def in Clojure.

Runtimes (1)