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

  1. Do the set! on the same thread that created the binding; rebind inside the worker: (binding [*x* v] ...) in the future body
  2. Capture needed values outside the future and pass them as arguments instead of relying on set!
  3. Use an atom (swap!) when cross-thread mutation is the actual requirement
  4. 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

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


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)