{"record":{"id":"2788713fd8f7380f","repo":"clojure/clojure","slug":"can-only-recur-from-tail-position","errorCode":null,"errorMessage":"Can only recur from tail position","messagePattern":"Can only recur from tail position","errorType":"exception","errorClass":"java.lang.UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"src/jvm/clojure/lang/Compiler.java","lineNumber":7255,"sourceCode":"\tpublic Class getJavaClass() {\n\t\treturn RECUR_CLASS;\n\t}\n\n\tstatic class Parser implements IParser{\n\t\tpublic Expr parse(C context, Object frm) {\n\t\t\tint line = lineDeref();\n\t\t\tint column = columnDeref();\n\t\t\tString source = (String) SOURCE.deref();\n\n\t\t\t// In :once fn, recur to head invalidates :once\n\t\t\tObjMethod method = (ObjMethod)METHOD.deref();\n\t\t\tif(method.objx.onceOnly && method.clearRoot == CLEAR_ROOT.deref())\n\t\t\t\tmethod.objx.onceOnly = false;\n\n\t\t\tISeq form = (ISeq) frm;\n\t\t\tIPersistentVector loopLocals = (IPersistentVector) LOOP_LOCALS.deref();\n\t\t\tif(context != C.RETURN || loopLocals == null)\n\t\t\t\tthrow new UnsupportedOperationException(\"Can only recur from tail position\");\n                        if(NO_RECUR.deref() != null)\n                            throw new UnsupportedOperationException(\"Cannot recur across try\");\n\t\t\tPersistentVector args = PersistentVector.EMPTY;\n\t\t\tfor(ISeq s = RT.seq(form.next()); s != null; s = s.next())\n\t\t\t\t{\n\t\t\t\targs = args.cons(analyze(C.EXPRESSION, s.first()));\n\t\t\t\t}\n\t\t\tif(args.count() != loopLocals.count())\n\t\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\tString.format(\"Mismatched argument count to recur, expected: %d args, got: %d\",\n\t\t\t\t\t\t              loopLocals.count(), args.count()));\n\t\t\tfor(int i = 0;i< loopLocals.count();i++)\n\t\t\t\t{\n\t\t\t\tLocalBinding lb = (LocalBinding) loopLocals.nth(i);\n\t\t\t\tClass primc = lb.getPrimitiveType();\n\t\t\t\tif(primc != null)\n\t\t\t\t\t{\n\t\t\t\t\tboolean mismatch = false;","sourceCodeStart":7237,"sourceCodeEnd":7273,"githubUrl":"https://github.com/clojure/clojure/blob/f3b143341d6efc6428b523b2eaa099a6cc99156e/src/jvm/clojure/lang/Compiler.java#L7237-L7273","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Restructure so recur is the last expression of the loop/fn body branch, e.g. wrap non-tail work before the recur call.","Replace non-tail recur with explicit loop state updates: bind intermediate results to locals and recur once at the end.","Use loop/recur-compatible tail style or fall back to recursion with trampolining for non-tail cases.","If recur is inside a callback/fn literal, move it into its own loop within that fn."],"exampleFix":"// before\n(loop [x 1]\n  (if (< x 5)\n    (do (println x)\n        (+ 1 x)          ; non-tail expression before recur\n        (recur x))))\n// after\n(loop [x 1]\n  (if (< x 5)\n    (do (println x)\n        (recur (inc x))) ; recur in tail position\n    x))","handlingStrategy":"validation","validationCode":";; lint before compile: recur must be last expr of loop/fn body branch\n(defn recur-tail? [form]\n  (every? (fn [[_ body]] (= 'recur (first (last body))))\n          (filter vector? (rest form))))","typeGuard":"function valid-recur-usage? [form] (and (seq? form) (= 'recur (first form)) (loop-enclosing?))","tryCatchPattern":"try\n  (compile/eval form)\ncatch UnsupportedOperationException e\n  (when (.getMessage e) (throw (ex-info \"non-tail recur\" {:form form} e))))","preventionTips":["Keep loop bodies structured as (if cond (recur new-state) result).","Run clj-kondo or eastwood; both flag non-tail recur.","Avoid recur inside callbacks, map/filter fns, and non-tail expressions."],"tags":["clojure","compiler","recur","tail-position"],"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"}