Learn the real differences between `cond`, `case`, and `cond->`, and how to choose the right one for branching versus conditional transformation.
cond,case, andcond->: Three different tools that look related but solve different problems: sequential predicate testing, constant-value dispatch, and conditional transformation of one value.
These forms are often taught together because they all reduce nested if chains. But idiomatic Clojure depends on choosing the right one for the shape of the decision you are making. If you treat them as interchangeable, the code usually becomes either slower, less readable, or both.
cond for Ordered Predicate Checkscond is the general-purpose tool for evaluating a series of conditions in order.
1(defn classify-temperature [c]
2 (cond
3 (< c 0) :freezing
4 (< c 10) :cold
5 (< c 20) :cool
6 (< c 30) :warm
7 :else :hot))
This reads well because each branch expresses a real predicate. The ordering matters. Once a truthy condition is found, cond returns that branch and stops.
Use cond when:
case for Constant Dispatchcase is more specialized. It dispatches on the value of one expression against constant choices.
1(defn day-type [day]
2 (case day
3 :saturday :weekend
4 :sunday :weekend
5 :monday :workday
6 :tuesday :workday
7 :wednesday :workday
8 :thursday :workday
9 :friday :workday
10 :unknown))
This is the right choice when the question is:
“Given this exact value, which branch do I want?”
It is usually clearer and more direct than a long cond full of = checks.
case to Do Predicate Workcase is not a general-purpose branching tool. It is for constant matching, not arbitrary logic.
Bad use:
1(case amount
2 (> amount 100) :large
3 :small)
That is conceptually wrong because case does not evaluate those branch labels as predicates. When the branch depends on relational logic, type checks, or combinations of conditions, cond is the right tool.
cond-> Is Not a Branching Formcond-> is frequently misunderstood because of its name. It does not pick one branch. It conditionally applies multiple transformations to the same starting value.
1(defn build-query [params]
2 (cond-> {:limit 50}
3 (:status params) (assoc :status (:status params))
4 (:sort-by params) (assoc :sort-by (:sort-by params))
5 (:admin? params) (assoc :include-deleted true)))
Each truthy test adds another transformation. That makes cond-> a good fit for:
It is the wrong choice when you mean “pick one branch and stop.”
cond-> Does Not Short-CircuitThis is the nuance many people miss: cond-> evaluates every test-expression pair in order. If three tests are truthy, all three transformations happen.
That makes it different from:
cond, which stops at the first matching branchsome->, which stops on nilUnderstanding this difference prevents subtle bugs in request or config builders.
flowchart TD
A["What kind of decision is this?"] --> B{"One value matched to fixed constants?"}
B -- Yes --> C["Use `case`"]
B -- No --> D{"Ordered predicates?"}
D -- Yes --> E["Use `cond`"]
D -- No --> F{"Building or transforming one value conditionally?"}
F -- Yes --> G["Use `cond->` or `cond->>`"]
It is possible to write all three forms in dense ways that save a few lines but cost readability. That usually happens when:
cond branches hide too much work inlinecase contains many unrelated outcomes and should really be a lookup mapcond-> includes complicated expressions instead of small transformationsIf one branch needs substantial logic, it is often better to call a named function from the branch rather than put everything inline.
cond for long chains of exact constant equality where case is clearercase with predicates or relational checkscond-> chooses only one branchcond is for ordered predicate checks.case is for constant-value dispatch.cond-> conditionally transforms one value and can apply multiple steps.