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

  1. Use unqualified symbols in bindings: f instead of my.ns/f.
  2. Inside macros, unqualify generated names with (with-local-vars ...) conventions or use plain (gensym) instead of syntax-quoted symbols.
  3. 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

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


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)