~ / track B / clojure advanced
Futures, promises, and delay
IntermediateBeyond atom / ref / agent ([[state-atoms-refs-agents]]) and
core.async ([[core-async]]), Clojure ships three small concurrency
primitives that show up constantly in everyday code: delay (compute
once, lazily, cached), future (compute now, on another thread, await
later), and promise (a write-once box that any thread can deliver
into). They share the IDeref interface, so you read all three the same
way: @x (sugar for (deref x)).
The three at a glance
| Primitive | When does it compute? | Who computes? | Can it be set? |
|---|---|---|---|
delay | First @ call | The dereffer's thread | No — captured at construction |
future | Immediately on construction | A background thread (in clojure.core/agent-pool) | No — captured at construction |
promise | Never automatically | A different thread calling (deliver p v) | Yes, once |
All three block on @ until the value is available (delays are
synchronous; futures are awaited; promises wait for delivery). All three
cache the first result.
delay — call-by-need at the value level
(def heavy
(delay
(println "computing!")
(reduce + (range 1e7))))
@heavy ;; prints, returns the sum
@heavy ;; cached, no print
A delay is a thunk plus memoization. Use it for expensive
initialization that may not be needed: a regex pattern that's only used
on certain code paths, a connection that may or may not be opened.
force is a synonym for deref on a delay. realized? tests whether it's
been computed.
future — fire-and-await
(def f (future (Thread/sleep 1000) :ready))
(realized? f) ;; => false
@f ;; blocks ~1s, then => :ready
@f ;; cached, returns immediately
The body runs on a thread from clojure.core/agent-pool now. deref
waits for it; an exception inside the future is rethrown on deref.
future-cancel will attempt interruption.
Use futures for bounded, independent I/O — fetching N URLs in parallel:
(let [urls ["a.com" "b.com" "c.com"]
fs (mapv #(future (slurp %)) urls)]
(mapv deref fs))
For truly large fan-out, prefer a structured pool (e.g.
claypoole) — uncontrolled future proliferation can saturate the agent
pool.
promise — coordination between threads
(def p (promise))
;; Thread A: waits for value
(future (println "got:" @p))
;; Thread B: delivers
(deliver p 42)
;; (thread A prints "got: 42")
A promise has no body. It's an empty box; the consumer derefs and blocks,
the producer calls (deliver p value) exactly once. Subsequent
delivers are silently ignored.
This is the building block for one-shot callbacks and for adapting callback APIs to blocking APIs:
(defn fetch [url]
(let [p (promise)]
(http/get url {} (fn [resp] (deliver p resp)))
@p)) ;; convert async-with-callback into sync-blocking
Try it
Choosing between them
Need a value right away, computed eagerly? → just call the fn
Need a value eventually, computed on demand? → delay
Need parallelism, value will be ready later? → future
Need cross-thread handoff, no compute body? → promise
Need ongoing streaming or backpressure? → core.async chan
Need shared mutable state? → atom / ref / agent
Exception semantics
@(future (throw (ex-info "boom" {})))
;; => throws ExecutionException wrapping the original at deref time
A delayed-thrown exception in a future is held until deref, then thrown.
This is sometimes called the "thrown-on-realize" trap — silent failure
until somebody reads the value. Test futures by always derefencing in the
end.
Real-world
| Pattern | Where |
|---|---|
| Parallel HTTP fan-out for fan-in aggregation | API gateways aggregating microservice responses |
delay around a singleton schema/regex | Avoids JVM startup cost when the value isn't always needed |
promise as a bridge between callback and blocking APIs | Adapting clj-http async to sync code, core.async channels to non-async callers |
(deref f timeout-ms timeout-val) | Bounded waits with a fallback ("if this fetch isn't back in 5s, use the cached value") |
realized? for status checks | Health-check endpoints that report "warmup is/isn't done" |
Test mocks: (deliver p :ok) from inside a fixture | Coordinating "the background process did its thing before we assert" |
Check yourself
? quiz
You have three independent HTTP calls and want to issue them in parallel and collect all three results. Which primitive(s) fit best?
Exercise
Sketch a function with-timeout that takes a no-arg function f and a
millisecond timeout, runs f on a future, and returns either its result or
:timeout if f doesn't finish in time. Hint: (deref fut ms timeout-val).
Now extend it: if :timeout was returned, cancel the future. What happens
if f is in the middle of a Java read() call when you cancel? (Hint: not
all I/O is interruptible.)