nil, truthy values, and nil-punning
BasicpracticeClojure'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) ;; => :truthyIf you're coming from Python or Ruby, the surprising one is (if [] :t :f)
returning :truthy. Use seqclojure.core/seqReturn a seq view of coll, or nil if empty.view on clojuredocs → 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)nextclojure.core/nextItems after the first, or nil if there are none.view on clojuredocs → vs restclojure.core/restItems after the first as a lazy seq.view on clojuredocs →: nextclojure.core/nextItems after the first, or nil if there are none.view on clojuredocs → returns nil on an empty seq; restclojure.core/restItems after the first as a lazy seq.view on clojuredocs →
returns (). Use nextclojure.core/nextItems after the first, or nil if there are none.view on clojuredocs → when you want a recursion guard that ends on the
empty list (check with when-letclojure.core/when-letBind only when the test value is truthy.view on clojuredocs → or when-some).
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->clojure.core/some->Threading that short-circuits on nil.view on clojuredocs → and some->>clojure.core/some->>Last-arg threading that short-circuits on nil.view on clojuredocs →, you get short-circuiting "if anything
returned nil, stop":
That's Clojure's version of the optional-chaining pattern — like ?. in
JavaScript or Swift — short-circuiting on nil so you don't write an
if-check at every step.
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/falseif-someclojure.core/if-someLike if-let, but only nil (not false) falls through to else.view on clojuredocs → 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-letclojure.core/if-letLike let, but only enters the then branch when bound value is truthy.view on clojuredocs → instead of if-someclojure.core/if-someLike if-let, but only nil (not false) falls through to else.view on clojuredocs →.
andclojure.core/andShort-circuiting logical and; returns the first falsy or the last truthy.view on clojuredocs →, orclojure.core/orShort-circuiting logical or; returns the first truthy or last value.view on clojuredocs →, nil, and short-circuiting
andclojure.core/andShort-circuiting logical and; returns the first falsy or the last truthy.view on clojuredocs → and orclojure.core/orShort-circuiting logical or; returns the first truthy or last value.view on clojuredocs → 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->clojure.core/some->Threading that short-circuits on nil.view on clojuredocs → or check:
(some-> s .length) ;; => nil if s is nil
(when s (.length s)) ;; equivalentReal-world
| Pattern | Where it shows up |
|---|---|
| some->clojure.core/some->Threading that short-circuits on nil.view on clojuredocs → 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 |
| seqclojure.core/seqReturn a seq view of coll, or nil if empty.view on clojuredocs → to test "non-empty" | Loop termination, "should I render this section?" |
| if-someclojure.core/if-someLike if-let, but only nil (not false) falls through to else.view on clojuredocs → for boolean-valued options | Feature flags, debug toggles |
nil-tolerant pipelines (mapclojure.core/mapApply f to each element, returning a lazy seq.view on clojuredocs →, filterclojure.core/filterLazy seq of items where (pred x) is truthy.view on clojuredocs →, concatclojure.core/concatLazy concatenation of the given collections.view on clojuredocs →) | 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->clojure.core/some->Threading that short-circuits on nil.view on clojuredocs → 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-letclojure.core/if-letLike let, but only enters the then branch when bound value is truthy.view on clojuredocs → and letclojure.core/letLocal bindings: (let [k v …] body).view on clojuredocs →. Compare the
character counts and the readability.