clojure/clojure · error · java.lang.UnsupportedOperationException

Can only recur from tail position

Error message

Can only recur from tail position

What it means

Clojure requires that recur appear in the tail position of its enclosing loop, fn, or letfn. The compiler's RecurExpr analyzer checks that the current compile context is C.RETURN (tail position) and that LOOP_LOCALS is bound; if either fails, it throws this UnsupportedOperationException, meaning the recur would not actually be the last operation.

Solutions

  1. Restructure so recur is the last expression of the loop/fn body branch, e.g. wrap non-tail work before the recur call.
  2. Replace non-tail recur with explicit loop state updates: bind intermediate results to locals and recur once at the end.
  3. Use loop/recur-compatible tail style or fall back to recursion with trampolining for non-tail cases.
  4. If recur is inside a callback/fn literal, move it into its own loop within that fn.

Example fix

// before
(loop [x 1]
  (if (< x 5)
    (do (println x)
        (+ 1 x)          ; non-tail expression before recur
        (recur x))))
// after
(loop [x 1]
  (if (< x 5)
    (do (println x)
        (recur (inc x))) ; recur in tail position
    x))
Defensive patterns

Strategy: validation

Validate before calling

;; lint before compile: recur must be last expr of loop/fn body branch
(defn recur-tail? [form]
  (every? (fn [[_ body]] (= 'recur (first (last body))))
          (filter vector? (rest form))))

Type guard

function valid-recur-usage? [form] (and (seq? form) (= 'recur (first form)) (loop-enclosing?))

Try / catch

try
  (compile/eval form)
catch UnsupportedOperationException e
  (when (.getMessage e) (throw (ex-info "non-tail recur" {:form form} e))))

Prevention

When it happens

Trigger: Writing (recur ...) in a non-tail position, e.g. (if test (recur x)) missing... actually: recur inside (when ... non-tail), inside (str ... ), (map (recur ...)), after which other expressions run, or where LOOP_LOCALS is null because there is no enclosing loop/fn with matching locals.

Common situations: Putting recur in the middle of an expression like (if cond (recur x) (recur y)) is fine, but (recur (recur x)), recur inside a fn passed to map/filter, or recur inside when-not bodies that aren't tail; also recur at top level without loop.

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/2788713fd8f7380f. Report an issue: GitHub.

Appendix: source

Thrown at src/jvm/clojure/lang/Compiler.java:7255

	public Class getJavaClass() {
		return RECUR_CLASS;
	}

	static class Parser implements IParser{
		public Expr parse(C context, Object frm) {
			int line = lineDeref();
			int column = columnDeref();
			String source = (String) SOURCE.deref();

			// In :once fn, recur to head invalidates :once
			ObjMethod method = (ObjMethod)METHOD.deref();
			if(method.objx.onceOnly && method.clearRoot == CLEAR_ROOT.deref())
				method.objx.onceOnly = false;

			ISeq form = (ISeq) frm;
			IPersistentVector loopLocals = (IPersistentVector) LOOP_LOCALS.deref();
			if(context != C.RETURN || loopLocals == null)
				throw new UnsupportedOperationException("Can only recur from tail position");
                        if(NO_RECUR.deref() != null)
                            throw new UnsupportedOperationException("Cannot recur across try");
			PersistentVector args = PersistentVector.EMPTY;
			for(ISeq s = RT.seq(form.next()); s != null; s = s.next())
				{
				args = args.cons(analyze(C.EXPRESSION, s.first()));
				}
			if(args.count() != loopLocals.count())
				throw new IllegalArgumentException(
						String.format("Mismatched argument count to recur, expected: %d args, got: %d",
						              loopLocals.count(), args.count()));
			for(int i = 0;i< loopLocals.count();i++)
				{
				LocalBinding lb = (LocalBinding) loopLocals.nth(i);
				Class primc = lb.getPrimitiveType();
				if(primc != null)
					{
					boolean mismatch = false;

View on GitHub (pinned to f3b143341d)