Learn how `recur` makes specific recursive shapes stack-safe in Clojure, and why it is not the same thing as general automatic tail-call optimization.
recur: A special form that performs a local jump back to the nearestloopor function arity when used in tail position, making that recursion stack-safe.
One of the most important Clojure recursion rules is also one of the most misunderstood: Clojure does not provide general automatic tail-call optimization for arbitrary recursive calls. Instead, it provides recur, which is explicit, local, and predictable.
recur Actually Doesrecur does not call a function the ordinary way. It rebinds the local recursion point with new values and jumps back without growing the stack.
That makes it ideal for:
1(defn factorial [n]
2 (loop [n n
3 acc 1]
4 (if (zero? n)
5 acc
6 (recur (dec n) (* acc n)))))
This is stack-safe because recur is in tail position and targets the loop.
recur must be the final action in its branch. This fails conceptually:
1;; not valid use of recur
2(if (zero? n)
3 acc
4 (+ 1 (recur (dec n) acc)))
The addition happens after the recursive step, so the recursive jump is not in tail position. Clojure rejects this because it cannot safely turn it into a stack-neutral jump.
That restriction is a feature. It forces you to be explicit about the shape of the recursion.
loop and recur Belong TogetherThe most common pattern is loop/recur, which gives you iterative control flow while staying inside functional local bindings.
1(defn sum-xs [xs]
2 (loop [remaining xs
3 total 0]
4 (if (empty? remaining)
5 total
6 (recur (rest remaining)
7 (+ total (first remaining))))))
This is often clearer than using raw Java-style looping constructs because the state transitions stay explicit and immutable at each step.
recur Can Also Target the Current Function ArityYou do not always need an explicit loop. A function can recur to its own current arity:
1(defn countdown [n]
2 (if (zero? n)
3 :done
4 (recur (dec n))))
This works because the recursion point is the function arity itself. But once the state gets richer, loop often reads more clearly because the state variables are grouped visibly at the top.
Many recursive problems become recur-friendly only after introducing an accumulator.
1(defn reverse-xs [xs]
2 (loop [remaining xs
3 acc '()]
4 (if (empty? remaining)
5 acc
6 (recur (rest remaining)
7 (cons (first remaining) acc)))))
This is a standard technique: move deferred work into explicit state so the recursive jump can be the last step.
recur Is Not for Mutual RecursionBecause recur only targets the nearest local recursion point, it does not solve mutual recursion between separate functions. If two functions call each other, recur cannot jump between them.
That means you should not think of recur as universal recursion optimization. It is a local control-flow construct with strict rules.
flowchart TD
A["Recursive problem"] --> B{"Can the next step be the final action?"}
B -- Yes --> C["Use `recur` with explicit state"]
B -- No --> D["Refactor with accumulators or another approach"]
C --> E["Stack-safe local recursion"]
Not every loop should become manual recur. If the goal is already well captured by:
mapfilterreducetransducesomethen those are often better than handwritten recursion. recur is most helpful when the state transition is specific enough that the higher-level helper would hide more than it clarifies.
recur outside tail positionreduce or another sequence helper already fitsrecur for mutual recursion between different functionsrecur gives explicit local tail recursion, not global automatic TCO.loop/recur is the most common stack-safe iteration pattern.recur-friendly.