clojure/clojure · error · clojure.lang.Util+runtimeException
Can't let qualified name:
Error message
Can't let qualified name:
What it means
letfn* locals must be unqualified symbols. If a binding name carries a namespace (e.g. my.ns/f or foo/bar), the compiler throws a RuntimeException because namespace-qualified names cannot become locals.
Solutions
- Use unqualified symbols in bindings: f instead of my.ns/f.
- Inside macros, unqualify generated names with (with-local-vars ...) conventions or use plain (gensym) instead of syntax-quoted symbols.
- Check macro expansion with macroexpand-1 and strip namespaces via (symbol (name sym)) where appropriate.
Example fix
// before `(letfn* [my.ns/f (fn [x] x)] (f 1)) // after (letfn* [f (fn [x] x)] (f 1))
Defensive patterns
Strategy: validation
Validate before calling
(doseq [n (take-nth 2 bindings)]
(when (and (symbol? n) (namespace n))
(throw (ex-info "binding names must be unqualified" {:name n})))) Type guard
(defn unqualified-bindings? [bindings] (every? #(and (symbol? %) (nil? (namespace %))) (take-nth 2 bindings)))
Try / catch
(try (eval form)
(catch RuntimeException e
(when (.startsWith (.getMessage e) "Can't let qualified name")
(log/error "qualified local name in" form)))) Prevention
- In macros use gensym/auto-gensym (x#) instead of quoted qualified symbols
- Avoid syntax-quoting names meant to be locals
- Strip namespaces from data-driven identifiers with (symbol (name s))
When it happens
Trigger: Compiling letfn* with a qualified symbol as a binding name, e.g. (letfn* [my.ns/f (fn [x] x)] ...), typically from generated or macro-expanded code that resolves symbols too early.
Common situations: Macros that build local names inside syntax-quote causing auto-resolved/qualified symbols (ns`foo), or code that stores fully-qualified names in data used for bindings.
Related errors
- Can't create defs outside of current ns
- Can't refer to qualified var that doesn't exist
- Can't use & as a local binding
- Can't use qualified name as parameter:
- ArityException
AI-assisted analysis of clojure/clojure@f3b143341d (2026-09-09).
Data as JSON: /api/errors/5c05809c66559dae.
Report an issue: GitHub.
Appendix: source
Thrown at src/jvm/clojure/lang/Compiler.java:6818
return analyze(context, RT.list(RT.list(FNONCE, PersistentVector.EMPTY, form)));
IPersistentMap dynamicBindings = RT.map(LOCAL_ENV, LOCAL_ENV.deref(),
NEXT_LOCAL_NUM, NEXT_LOCAL_NUM.deref());
try
{
Var.pushThreadBindings(dynamicBindings);
//pre-seed env (like Lisp labels)
PersistentVector lbs = PersistentVector.EMPTY;
for(int i = 0; i < bindings.count(); i += 2)
{
if(!(bindings.nth(i) instanceof Symbol))
throw new IllegalArgumentException(
"Bad binding form, expected symbol, got: " + bindings.nth(i));
Symbol sym = (Symbol) bindings.nth(i);
if(sym.getNamespace() != null)
throw Util.runtimeException("Can't let qualified name: " + sym);
LocalBinding lb = registerLocal(sym, tagOf(sym), null,false);
lb.canBeCleared = false;
lbs = lbs.cons(lb);
}
PersistentVector bindingInits = PersistentVector.EMPTY;
for(int i = 0; i < bindings.count(); i += 2)
{
Symbol sym = (Symbol) bindings.nth(i);
Expr init = analyze(C.EXPRESSION, bindings.nth(i + 1), sym.name);
LocalBinding lb = (LocalBinding) lbs.nth(i / 2);
lb.init = init;
BindingInit bi = new BindingInit(lb, init);
bindingInits = bindingInits.cons(bi);
}
return new LetFnExpr(bindingInits, (new BodyExpr.Parser()).parse(context, body));
}
finally
{View on GitHub (pinned to f3b143341d)