{"record":{"id":"fcf8bceb87f1d8ec","repo":"clojure/clojure","slug":"can-t-dynamically-bind-non-dynamic-var-s-s","errorCode":null,"errorMessage":"Can't dynamically bind non-dynamic var: %s/%s","messagePattern":"Can't dynamically bind non-dynamic var: (.+?)/(.+?)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"src/jvm/clojure/lang/Var.java","lineNumber":327,"sourceCode":"synchronized public Object alterRoot(IFn fn, ISeq args) {\n\tObject newRoot = fn.applyTo(RT.cons(root, args));\n\tvalidate(getValidator(), newRoot);\n\tObject oldroot = root;\n\tthis.root = newRoot;\n\t++rev;\n    notifyWatches(oldroot,newRoot);\n\treturn newRoot;\n}\n\npublic static void pushThreadBindings(Associative bindings){\n\tFrame f = dvals.get();\n\tAssociative bmap = f.bindings;\n\tfor(ISeq bs = bindings.seq(); bs != null; bs = bs.next())\n\t\t{\n\t\tIMapEntry e = (IMapEntry) bs.first();\n\t\tVar v = (Var) e.key();\n\t\tif(!v.dynamic)\n\t\t\tthrow new IllegalStateException(String.format(\"Can't dynamically bind non-dynamic var: %s/%s\", v.ns, v.sym));\n\t\tv.validate(v.getValidator(), e.val());\n\t\tv.threadBound.set(true);\n\t\tbmap = bmap.assoc(v, new TBox(Thread.currentThread(), e.val()));\n\t\t}\n\tdvals.set(new Frame(bmap, f));\n}\n\npublic static void popThreadBindings(){\n    Frame f = dvals.get().prev;\n    if (f == null) {\n        throw new IllegalStateException(\"Pop without matching push\");\n    } else if (f == Frame.TOP) {\n        dvals.remove();\n    } else {\n        dvals.set(f);\n    }\n}\n","sourceCodeStart":309,"sourceCodeEnd":345,"githubUrl":"https://github.com/clojure/clojure/blob/f3b143341d6efc6428b523b2eaa099a6cc99156e/src/jvm/clojure/lang/Var.java#L309-L345","documentation":"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.","triggerScenarios":"(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.","commonSituations":"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.","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"],"exampleFix":"// before\n(def *timeout* 100)\n(binding [*timeout* 500] ...) ; IllegalStateException\n// after\n(def ^:dynamic *timeout* 100)\n(binding [*timeout* 500] ...)","handlingStrategy":"validation","validationCode":"(when-not (-> var meta :dynamic)\n  (throw (ex-info \"var is not ^:dynamic\" {:var var})))","typeGuard":"(defn dynamic-var? [v] (boolean (:dynamic (meta v))))","tryCatchPattern":"(try (binding [v val] ...)\n  (catch IllegalStateException e\n    (with-redefs-fn {v (constantly val)} #(do-work))))","preventionTips":["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"],"tags":["clojure","dynamic-binding","var","metadata"],"backgroundTag":"invalid-state-transition","analyzedSha":"f3b143341d6efc6428b523b2eaa099a6cc99156e","analyzedAt":"2026-09-09T12:04:58.961Z","contentChangedAt":"2026-09-09T12:04:58.961Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}