clojure/clojure · error · IllegalStateException
Can't dynamically bind non-dynamic var
Error message
Can't dynamically bind non-dynamic var: %s/%s
What it means
Var.pushThreadBindings (used by the binding macro) validates that every Var being bound is declared dynamic. It throws IllegalStateException naming ns/sym when a non-dynamic var is passed to binding, since only ^:dynamic vars support thread-local rebinding in modern Clojure.
Solutions
- Add ^:dynamic to the var's def: (def ^:dynamic *foo* default)
- Remove the var from the binding form and pass the value as a function argument instead
- If you don't own the var, use with-redefs (testing only) rather than binding
- Pin library versions so upstream doesn't silently drop :dynamic
Example fix
// before (def *timeout* 100) (binding [*timeout* 500] ...) ; IllegalStateException // after (def ^:dynamic *timeout* 100) (binding [*timeout* 500] ...)
Defensive patterns
Strategy: validation
Validate before calling
(when-not (-> var meta :dynamic)
(throw (ex-info "var is not ^:dynamic" {:var var}))) Type guard
(defn dynamic-var? [v] (boolean (:dynamic (meta v))))
Try / catch
(try (binding [v val] ...)
(catch IllegalStateException e
(with-redefs-fn {v (constantly val)} #(do-work)))) Prevention
- Declare ^:dynamic on any var you plan to bind
- Only bind vars you own or know are dynamic
- Review upstream changes to var metadata on upgrades
When it happens
Trigger: (binding [regular-var v] ...) where the var lacks ^:dynamic; binding a var from another library that is not dynamic; a library upgrade dropped ^:dynamic from a var you bind.
Common situations: Copying binding forms referencing a var whose :dynamic metadata was removed (Clojure 1.3 made dynamic explicit); binding user-facing config vars that were never declared ^:dynamic.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Pop without matching push
- Attempting to call unbound fn: " + v
- Can't set!: from non-binding thread
- Can't take value of a macro:
- Expecting var, but is mapped to
AI-assisted analysis of clojure/clojure@f3b143341d (2026-09-09).
Data as JSON: /api/errors/fcf8bceb87f1d8ec.
Report an issue: GitHub.
Appendix: source
Thrown at src/jvm/clojure/lang/Var.java:327
synchronized public Object alterRoot(IFn fn, ISeq args) {
Object newRoot = fn.applyTo(RT.cons(root, args));
validate(getValidator(), newRoot);
Object oldroot = root;
this.root = newRoot;
++rev;
notifyWatches(oldroot,newRoot);
return newRoot;
}
public static void pushThreadBindings(Associative bindings){
Frame f = dvals.get();
Associative bmap = f.bindings;
for(ISeq bs = bindings.seq(); bs != null; bs = bs.next())
{
IMapEntry e = (IMapEntry) bs.first();
Var v = (Var) e.key();
if(!v.dynamic)
throw new IllegalStateException(String.format("Can't dynamically bind non-dynamic var: %s/%s", v.ns, v.sym));
v.validate(v.getValidator(), e.val());
v.threadBound.set(true);
bmap = bmap.assoc(v, new TBox(Thread.currentThread(), e.val()));
}
dvals.set(new Frame(bmap, f));
}
public static void popThreadBindings(){
Frame f = dvals.get().prev;
if (f == null) {
throw new IllegalStateException("Pop without matching push");
} else if (f == Frame.TOP) {
dvals.remove();
} else {
dvals.set(f);
}
}
View on GitHub (pinned to f3b143341d)