clojure/clojure · error · IllegalStateException
Can't set!: from non-binding thread
Error message
Can't set!: %s from non-binding thread
What it means
Var.set backs the set! special form on thread-bound dynamic Vars. It throws IllegalStateException when the current thread is not the thread that established the thread-local binding (the TBox records the owning thread), because mutating another thread's binding is undefined behavior in Clojure's binding model.
Solutions
- Do the set! on the same thread that created the binding; rebind inside the worker: (binding [*x* v] ...) in the future body
- Capture needed values outside the future and pass them as arguments instead of relying on set!
- Use an atom (swap!) when cross-thread mutation is the actual requirement
- Only use binding/set! for truly per-thread dynamic scope
Example fix
// before
(binding [*db* conn]
(future (set! *db* other-conn))) ; IllegalStateException
// after
(future
(binding [*db* other-conn]
...)) ; bind within the worker thread Defensive patterns
Strategy: try-catch
Validate before calling
(when-not (= (Thread/currentThread)
(:thread (get-in @clojure.lang.Var/dvals [:bindings v])))
(throw (ex-info "not binding thread" {:var v}))) Try / catch
(try (set! v val)
(catch IllegalStateException e
(binding [v val] (do-work)))) Prevention
- Never set! dynamic vars inside future/agent/executor code
- Rebind inside the worker thread instead
- Use atoms for cross-thread mutable state
When it happens
Trigger: Calling set! on a thread-bound dynamic var inside a future/agent/thread pool/executor task that runs on a different thread than the (binding [...]) push.
Common situations: Setting *out*, *in*, or a custom dynamic var inside a future or core.async thread (:!tp agent threads); logging config vars set in a worker thread; rebinding inside executors.
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
- AbortException
- Agent does not need a restart
- Agent is failed, needs restart
- Can't dynamically bind non-dynamic var
- Can't eval monitor-enter
AI-assisted analysis of clojure/clojure@f3b143341d (2026-09-09).
Data as JSON: /api/errors/0a94a6bdcbb47a19.
Report an issue: GitHub.
Appendix: source
Thrown at src/jvm/clojure/lang/Var.java:225
public void setValidator(IFn vf){
if(hasRoot())
validate(vf, root);
validator = vf;
}
public Object alter(IFn fn, ISeq args) {
set(fn.applyTo(RT.cons(deref(), args)));
return this;
}
public Object set(Object val){
validate(getValidator(), val);
TBox b = getThreadBinding();
if(b != null)
{
if(Thread.currentThread() != b.thread)
throw new IllegalStateException(String.format("Can't set!: %s from non-binding thread", sym));
return (b.val = val);
}
throw new IllegalStateException(String.format("Can't change/establish root binding of: %s with set", sym));
}
public Object doSet(Object val) {
return set(val);
}
public Object doReset(Object val) {
bindRoot(val);
return val;
}
public void setMeta(IPersistentMap m) {
//ensure these basis keys
resetMeta(m.assoc(nameKey, sym).assoc(nsKey, ns));
}View on GitHub (pinned to f3b143341d)