Flow control: if, when, cond, case
BasicMost Clojure code is small expressions threaded through a handful of
conditional and branching forms. There is no statement vs expression
distinction — everything returns a value, even if — 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 pred₁ v₁ pred₂ v₂ ...) | First matching vᵢ | Multi-way branch on arbitrary predicates |
(condp pred expr a r₁ b r₂ ...) | rᵢ where (pred expr xᵢ) truthy | Multi-way against one comparison |
(case expr v₁ r₁ v₂ r₂ ... default) | rᵢ 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-let, 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.
cond vs case vs condp
;; 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: case does not evaluate its left-hand values — they must be
literal constants. (case x my-keyword …) will not work the way you expect;
use cond or condp = instead.
do and side effects
if, when, let, fn each take a single expression as a body — but
when and let and fn implicitly wrap the body in do, so you can write
multiple forms. if does not:
(if cond
(do ;; explicit do needed
(println "going!")
(run))
(println "skipping"))
when and let are syntax sugar for (if test (do body) nil) and
(let [...] (do body)) — a tiny rule that explains a lot.
for is not a loop
for is a list comprehension — it returns a lazy seq, it does not run
for side effects. If you want to iterate for effect, use doseq:
(for [x (range 3) y (range 3)] [x y]) ;; produces a seq of pairs
(doseq [x (range 3)] (println x)) ;; prints, returns nil
For loops in the imperative sense, see loop/recur in [[recursion-and-recur]].
Try it in the REPL
Real-world
| Pattern | Where |
|---|---|
cond for state-machine transitions | Compojure/Reitit handlers, finite-state-machine libraries |
case for hot-path dispatch | Token parsing, opcode interpreters, performance-sensitive code |
condp for "which regex matches" | Lexers, URL routers, content-type negotiation |
if-let / when-let everywhere | Idiomatic "get-or-skip" patterns in CRUD code |
if-some / when-some | When the value false is meaningful (not just "missing") |
for for cartesian products / pipelines | Generating test fixtures, building combinations |
doseq 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
cond. - Using
condpwith<=. - Using
casefor 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?