Flow control: if, when, cond, case
BasicMost Clojure code is small expressions wired together with a handful of conditional and branching forms. There is no statement vs expression distinction — everything returns a value, even ifclojure.core/ifConditional: (if test then else?).view on clojuredocs → — so flow control composes cleanly inside other expressions.
The core forms
| Form | Returns | When to reach for it |
|---|---|---|
(if test then else) | then or else | One-shot branch with both arms |
(when test body...) | body or nil | Branch that only matters when test is truthy |
(when-not test body...) | body or nil | Same, inverted |
(cond pred1 v1 pred2 v2 ...) | First matching value | Multi-way branch on arbitrary predicates |
(condp pred expr a r1 b r2 ...) | Right-hand value where (pred expr left) is truthy | Multi-way against one comparison |
(case expr v1 r1 v2 r2 ... default) | Right-hand value for matching constant | Compile-time hash-dispatched switch |
(if-let [x expr] then else) | binds x if truthy, then then; else else | "Get the thing — if it exists, use it" |
(when-let [x expr] body...) | binds x if truthy, then body; else nil | Same as above without an else branch |
(if-some [x expr] ...) | Like if-letclojure.core/if-letLike let, but only enters the then branch when bound value is truthy.view on clojuredocs →, but only nil is false | Distinguishes false from missing |
(when-some [x expr] ...) | Same | Same |
Why so many?
Each form removes a tiny piece of noise that adds up. Compare:
;; if/let pyramid
(let [u (find-user id)]
(if u
(touch u)
nil))
;; with if-let
(if-let [u (find-user id)]
(touch u)
nil)
;; with when-let (drop the redundant nil branch)
(when-let [u (find-user id)]
(touch u))That last form is what idiomatic Clojure actually looks like.
if-letclojure.core/if-letLike let, but only enters the then branch when bound value is truthy.view on clojuredocs → vs if-someclojure.core/if-someLike if-let, but only nil (not false) falls through to else.view on clojuredocs → — the false trap
if-letclojure.core/if-letLike let, but only enters the then branch when bound value is truthy.view on clojuredocs → treats false as "not bound", so a legitimate false value falls
through to the else branch. if-someclojure.core/if-someLike if-let, but only nil (not false) falls through to else.view on clojuredocs → only treats nil that way, so false
counts as a real bound value:
(if-let [x false] :found :missing) ;; => :missing ← surprise!
(if-some [x false] :found :missing) ;; => :foundReach for if-someclojure.core/if-someLike if-let, but only nil (not false) falls through to else.view on clojuredocs → when false is a meaningful value (a flag, an option's
"off" state). For most CRUD-style "did the lookup return something?" code,
if-letclojure.core/if-letLike let, but only enters the then branch when bound value is truthy.view on clojuredocs → is fine because the alternative would be nil, not false.
condclojure.core/condMulti-way conditional with test → expr pairs.view on clojuredocs → vs caseclojure.core/caseConstant-time dispatch on compile-time literals.view on clojuredocs → vs condpclojure.core/condpcond with a fixed predicate against many values.view on clojuredocs →
;; cond: arbitrary predicates, top-to-bottom
(cond
(zero? n) :zero
(neg? n) :negative
(< n 100) :small
:else :large)
;; case: equality against compile-time constants (hash-dispatched, fast)
(case status
:ok "200 OK"
:missing "404 Not Found"
:error "500 Error"
"unknown") ;; trailing default
;; condp: one shared predicate against many right-hand values
(condp re-matches s
#"\d+" :digits
#"[a-z]+" :word
:other)Common bug: caseclojure.core/caseConstant-time dispatch on compile-time literals.view on clojuredocs → does not evaluate its left-hand values — they must be
literal constants. (case x my-keyword …) will not work the way you expect;
use condclojure.core/condMulti-way conditional with test → expr pairs.view on clojuredocs → or condp = instead.
doclojure.core/doEvaluate a sequence of forms for effect, return the last.view on clojuredocs → and side effects
whenclojure.core/when(when test body…) — evaluates body for side effects when test is truthy.view on clojuredocs →, letclojure.core/letLocal bindings: (let [k v …] body).view on clojuredocs →, and fnclojure.core/fnAnonymous function: (fn [args] body).view on clojuredocs → have implicit doclojure.core/doEvaluate a sequence of forms for effect, return the last.view on clojuredocs → bodies — you can stack multiple forms inside, and they run in order. ifclojure.core/ifConditional: (if test then else?).view on clojuredocs → does not: it takes exactly two arms (then / else), so to run multiple expressions in a branch you wrap them in doclojure.core/doEvaluate a sequence of forms for effect, return the last.view on clojuredocs → yourself:
(if cond
(do ;; explicit do needed
(println "going!")
(run))
(println "skipping"))whenclojure.core/when(when test body…) — evaluates body for side effects when test is truthy.view on clojuredocs → is sugar for (if test (do body) nil) — that's why it returns
nil when the test is false, and why multi-form bodies just work.
forclojure.core/forList comprehension; lazy. (for [x xs :when (p x)] (f x)).view on clojuredocs → is not a loop
forclojure.core/forList comprehension; lazy. (for [x xs :when (p x)] (f x)).view on clojuredocs → is a list comprehension — it returns a lazy seq, it does not run for side effects. If you want to iterate for effect, use doseqclojure.core/doseqLike for, but for side effects; returns nil.view on clojuredocs →:
(for [x (range 3) y (range 3)] [x y]) ;; produces a seq of pairs
(doseq [x (range 3)] (println x)) ;; prints, returns nilFor loops in the imperative sense, see loopclojure.core/looplet plus a recursion target for recur.view on clojuredocs →/recurclojure.core/recurTail-recursive call to the enclosing loop or fn.view on clojuredocs → in [[recursion-and-recur]].
Try it in the REPL
Real-world
| Pattern | Where |
|---|---|
| condclojure.core/condMulti-way conditional with test → expr pairs.view on clojuredocs → for state-machine transitions | Compojure/Reitit handlers, finite-state-machine libraries |
| caseclojure.core/caseConstant-time dispatch on compile-time literals.view on clojuredocs → for hot-path dispatch | Token parsing, opcode interpreters, performance-sensitive code |
| condpclojure.core/condpcond with a fixed predicate against many values.view on clojuredocs → for "which regex matches" | Lexers, URL routers, content-type negotiation |
| if-letclojure.core/if-letLike let, but only enters the then branch when bound value is truthy.view on clojuredocs → / when-letclojure.core/when-letBind only when the test value is truthy.view on clojuredocs → everywhere | Idiomatic "get-or-skip" patterns in CRUD code |
if-someclojure.core/if-someLike if-let, but only nil (not false) falls through to else.view on clojuredocs → / when-some | When the value false is meaningful (not just "missing") |
| forclojure.core/forList comprehension; lazy. (for [x xs :when (p x)] (f x)).view on clojuredocs → for cartesian products / pipelines | Generating test fixtures, building combinations |
| doseqclojure.core/doseqLike for, but for side effects; returns nil.view on clojuredocs → for side-effecting iteration | Database writes, logging, file output |
Check yourself
? quiz
Which form should you reach for when you have one expression to compare against many literal constants in a tight loop?
Exercise
Write three versions of an HTTP status classifier that maps 200 → :ok,
3xx → :redirect, 4xx → :client-error, 5xx → :server-error, anything
else → :unknown:
- Using condclojure.core/condMulti-way conditional with test → expr pairs.view on clojuredocs →.
- Using condpclojure.core/condpcond with a fixed predicate against many values.view on clojuredocs → with
<=. - Using caseclojure.core/caseConstant-time dispatch on compile-time literals.view on clojuredocs → for the canonical codes (200, 301, 400, 500), with a fallback to one of the above for the rest.
Which version do you actually want to read in six months?