Threading Macros for Readable Clojure Pipelines

Learn when to use `->`, `->>`, `some->`, `cond->`, and `as->` so Clojure pipelines stay readable and match function argument shape.

Threading macro: A macro that takes a starting value and inserts it into a sequence of forms so a transformation pipeline reads top to bottom instead of inside out.

Threading macros are idiomatic Clojure because they make transformation flow visible. They do not make code “more functional” by themselves. They simply make it easier to see how a value moves through a series of steps. That matters a lot in a language where data is often transformed through many small functions.

Why Threading Helps

Without threading, multi-step transformations quickly turn into nested calls that read from the inside out.

1(update (assoc person :hair-color :gray) :age inc)

That is valid, but the reader has to mentally unwind it. The thread-first macro -> makes the same transformation read in application order:

1(-> person
2    (assoc :hair-color :gray)
3    (update :age inc))

The main gain is not fewer characters. It is that the data flow is now obvious.

-> for Data-Structure Style APIs

Use -> when the value being transformed belongs in the first argument position. That is common for functions such as:

  • assoc
  • update
  • dissoc
  • get
  • Java interop method calls
1(-> {:name "Ava" :age 39}
2    (assoc :role :admin)
3    (update :age inc)
4    (dissoc :temp-token))

This style works well for maps and other data-structure transformations because most of the relevant core functions take the structure first.

->> for Sequence Pipelines

Use ->> when the value belongs in the last argument position. That is the normal shape for many sequence functions:

  • map
  • filter
  • remove
  • take
  • reduce
1(->> (range 10)
2     (filter odd?)
3     (map #(* % %))
4     (reduce +))

The rule of thumb is simple: if the collection naturally sits at the end of each call, ->> usually reads best.

Pick the Macro by Function Shape, Not by Habit

The most common mistake is choosing a threading macro because it looks familiar rather than because it matches the argument position of the functions involved.

1(->> {:name "Ava" :age 39}
2     (assoc :role :admin))

That is wrong because assoc expects the map first, not last. The fix is not mystical. It is simply to use the macro that matches the function signature.

some-> and some->> for Nil-Sensitive Pipelines

some-> and some->> are useful when a pipeline should stop if one stage produces nil.

1(some-> request
2        :headers
3        (get "x-request-id")
4        parse-request-id)

If any stage returns nil, the rest of the pipeline is skipped and the whole expression becomes nil.

This is especially useful around:

  • optional map keys
  • Java interop that would throw on nil
  • parsing or lookup chains where “missing” is an acceptable outcome

Do not use some-> just because it exists. Use it when nil is a legitimate short-circuit condition.

cond-> and cond->> for Conditional Transforms

cond-> is not a conditional branch like cond. It conditionally applies transformations to the same value.

1(cond-> {:name "Ava"}
2  include-role? (assoc :role :admin)
3  include-id?   (assoc :id "u-42"))

That makes it ideal for building maps, queries, and option structures where each transformation is optional.

Important nuance: cond-> does not short-circuit after the first matching condition. It evaluates each test and applies each matching transformation in order.

as-> When Neither First Nor Last Works

Sometimes the value belongs in different argument positions at different steps. That is what as-> is for.

1(as-> [:foo :bar] value
2  (map name value)
3  (first value)
4  (.substring value 1))

as-> is more flexible, but also less instantly readable than -> or ->>. Use it when the pipeline genuinely has mixed insertion points, not as a default.

    flowchart TD
	    A["Start with a value"] --> B{"Where does the value go?"}
	    B --> C["First argument -> use `->`"]
	    B --> D["Last argument -> use `->>`"]
	    B --> E["Only continue while non-nil -> use `some->` or `some->>`"]
	    B --> F["Apply optional transforms -> use `cond->` or `cond->>`"]
	    B --> G["Mixed positions -> use `as->`"]

When Not to Thread

Threading is not automatically clearer than everything else.

Avoid it when:

  • the pipeline is only one simple call
  • intermediate names would teach the reader more than a long chain
  • side effects dominate the code and the pipeline shape hides the real logic
  • the macro choice becomes harder to understand than the nested form

Sometimes a small let with named intermediate values is the more readable move.

Common Mistakes

  • using ->> for APIs that expect the value first
  • using -> for sequence transformations that want the collection last
  • assuming cond-> behaves like cond
  • overusing as-> when ordinary refactoring would be clearer
  • building long pipelines that should really be broken into named steps

Key Takeaways

  • -> is usually for data-structure transformations.
  • ->> is usually for sequence pipelines.
  • some-> helps when nil should stop the pipeline.
  • cond-> conditionally applies multiple transforms to one value.
  • as-> is the escape hatch for mixed insertion points.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026