nil, truthy values, and nil-punning
BasicClojure's truthiness rule is one of the simplest in any language:
nil and false are falsy. Everything else — including 0, "", [],
{}, #{} — is truthy. This single rule, combined with the way the core
library handles nil, gives Clojure a distinctive style called
nil-punning: write the happy path, let nil flow through, and short-
circuit at the edges.
The rule
(if nil :truthy :falsy) ;; => :falsy
(if false :truthy :falsy) ;; => :falsy
(if 0 :truthy :falsy) ;; => :truthy
(if "" :truthy :falsy) ;; => :truthy
(if [] :truthy :falsy) ;; => :truthy
(if {} :truthy :falsy) ;; => :truthy
If you're coming from Python or Ruby, the surprising one is (if [] :t :f)
returning :truthy. Use seq to ask "is it empty?":
(if (seq xs) :has-items :empty)
The reason: an empty collection is still a collection. Emptiness is a property to ask about, not a default-falsiness behavior.
What returns nil and what doesn't
Many core operations return nil for "no result," which makes nil-punning
work:
(get {:a 1} :missing) ;; => nil
(first []) ;; => nil
(rest []) ;; => () — empty seq, NOT nil
(next []) ;; => nil — `next` is `nil`-aware where `rest` isn't
(some odd? [2 4 6]) ;; => nil
(re-find #"\d" "abc") ;; => nil
({:a 1} :missing :fallback) ;; => :fallback (maps take a default arg)
The next vs rest distinction trips everyone once. Rule of thumb: if
you want a recursion guard that ends on the empty list, use next (returns
nil) and check with when-let.
nil-punning idioms
Because (map f nil) → (), (filter p nil) → (), (count nil) → 0,
(seq nil) → nil, much of the standard library accepts nil as "the empty
thing":
(count nil) ;; => 0
(empty? nil) ;; => true
(map inc nil) ;; => ()
(reduce + nil) ;; => 0
(concat nil [1 2]) ;; => (1 2)
This means you can pass nil through pipelines without checking for it.
Combined with some-> and some->>, you get short-circuiting "if anything
returned nil, stop":
That's Clojure's Maybe monad written out in two characters.
nil vs false — when the distinction matters
For booleans, two operators behave differently:
(if x ...) ;; treats nil and false the same
(if (some? x) ...) ;; true iff x is NOT nil (false counts as present)
(if (boolean x) ...) ;; coerces to true/false
if-some and when-some exist precisely so you can write "if a value is
present (not nil), even if it's false":
(if-some [v (get config :debug)]
(use-debug v) ;; runs even if v is false
(default-debug))
A common bug: storing false as a legitimate config value, then losing it
because the code uses if-let instead of if-some.
and, or, nil, and short-circuiting
and and or return values, not booleans — they short-circuit and
return the deciding value:
(or nil false 7 10) ;; => 7 ;; first truthy
(and 1 2 3 nil 5) ;; => nil ;; first falsy
(or x default) ;; idiom: "x if present, otherwise default"
(and x (.something x)) ;; idiom: "call .something only if x non-nil"
The (or x default) idiom is everywhere; it's the analog of ?? in
JavaScript/Swift, except it falls through on false too — which is
sometimes a bug source, again.
Avoiding NPEs from Java interop
Java doesn't share the nil discipline. Calling .length on nil throws
NPE. Wrap with some-> or check:
(some-> s .length) ;; => nil if s is nil
(when s (.length s)) ;; equivalent
Real-world
| Pattern | Where it shows up |
|---|---|
some-> for nested-map navigation | Reading config, parsing API responses, traversing parsed JSON |
(or x default) for optional values | Argument defaults, env-var reading, config merging |
seq to test "non-empty" | Loop termination, "should I render this section?" |
if-some for boolean-valued options | Feature flags, debug toggles |
nil-tolerant pipelines (map, filter, concat) | Aggregating partial results from concurrent calls |
Datomic / Datalog: missing attributes return nil | Pull queries silently omit absent attrs |
Reitit / Ring: middleware returns nil to skip | Auth middleware that returns the request unchanged is common |
Check yourself
? quiz
What does `(if [] :truthy :falsy)` return in Clojure, and why?
Exercise
Given the nested map:
(def order
{:id 42
:customer {:email "a@b.com" :address nil}
:items [{:sku "X1" :qty 3}]})
Use some-> to write a function order-city that returns the customer's
address city, returning nil cleanly if any link in the chain is missing.
Then write the same function using only if-let and let. Compare the
character counts and the readability.