~ / track B / clojure advanced
Futures, promises, and delay
IntermediateBeyond atomclojure.core/atomMutable, synchronous, uncoordinated reference type.view on clojuredocs → / refclojure.core/refCoordinated, synchronous reference; updated only inside dosync.view on clojuredocs → / agentclojure.core/agentAsynchronous, uncoordinated reference type; updated via send/send-off.view on clojuredocs → ([[state-atoms-refs-agents]]) and
core.async ([[core-async]]), Clojure ships three small concurrency
primitives that show up constantly in everyday code: delayclojure.core/delayLazy computation evaluated at most once on first deref.view on clojuredocs → (compute
once, lazily, cached), futureclojure.core/futureRun body on a thread pool; deref blocks until result is ready.view on clojuredocs → (compute now, on another thread, await
later), and promiseclojure.core/promiseA write-once container; readers block on deref until delivered.view on clojuredocs → (a write-once box that any thread can deliver
into). All three implement IDeref — the Clojure interface that makes
@x (sugar for (deref x)) work — so you read them the same way.
The three at a glance
| Primitive | When does it compute? | Who computes? | Can it be set? |
|---|---|---|---|
| delayclojure.core/delayLazy computation evaluated at most once on first deref.view on clojuredocs → | First @ call | The dereffer's thread | No — captured at construction |
| futureclojure.core/futureRun body on a thread pool; deref blocks until result is ready.view on clojuredocs → | Immediately on construction | A background thread in the agent send-off pool (clojure.lang.Agent/soloExecutor) | No — captured at construction |
| promiseclojure.core/promiseA write-once container; readers block on deref until delivered.view on clojuredocs → | 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.
delayclojure.core/delayLazy computation evaluated at most once on first deref.view on clojuredocs → — compute once, lazily (call-by-need at the value level)
(def heavy
(delay
(println "computing!")
(reduce + (range 1e7))))
@heavy ;; prints, returns the sum
@heavy ;; cached, no printA delayclojure.core/delayLazy computation evaluated at most once on first deref.view on clojuredocs → 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.
forceclojure.core/forceForce a delay; returns the cached value.view on clojuredocs → is a synonym for derefclojure.core/derefDereference a ref/atom/promise/future. Shorthand: @x.view on clojuredocs → on a delay. realized? tests whether it's
been computed.
futureclojure.core/futureRun body on a thread pool; deref blocks until result is ready.view on clojuredocs → — fire-and-await
(def f (future (Thread/sleep 1000) :ready))
(realized? f) ;; => false
@f ;; blocks ~1s, then => :ready
@f ;; cached, returns immediatelyThe body runs on a thread from the agent send-off pool now. derefclojure.core/derefDereference a ref/atom/promise/future. Shorthand: @x.view on clojuredocs →
waits for it; an exception inside the future is rethrown on derefclojure.core/derefDereference a ref/atom/promise/future. Shorthand: @x.view on clojuredocs →.
future-cancel will attempt interruption.
Use futures for bounded, independent I/O — fetching N URLs in parallel:
(let [urls ["https://a.example/" "https://b.example/" "https://c.example/"]
fs (mapv #(future (slurp %)) urls)]
(mapv deref fs))For truly large fan-out, prefer a structured pool (e.g.
claypoole) — uncontrolled futureclojure.core/futureRun body on a thread pool; deref blocks until result is ready.view on clojuredocs → proliferation can saturate the agent
pool.
promiseclojure.core/promiseA write-once container; readers block on deref until delivered.view on clojuredocs → — 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
deliverclojure.core/deliverProvide the value for a promise (exactly once).view on clojuredocs →s 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-blockingTry it
delayclojure.core/delayLazy computation evaluated at most once on first deref.view on clojuredocs → runs in the in-browser SCI REPL; futureclojure.core/futureRun body on a thread pool; deref blocks until result is ready.view on clojuredocs → and promiseclojure.core/promiseA write-once container; readers block on deref until delivered.view on clojuredocs → need real threads, so they're shown below as a fenced snippet for a JVM REPL.
;; Future: parallelism (JVM-only)
(def f (future (Thread/sleep 200) :done))
[(realized? f) @f] ;; blocks, then ready
;; Promise: coordination (JVM-only)
(def p (promise))
(future (deliver p (+ 1 2 3)))
@pChoosing 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 / agentException semantics
@(future (throw (ex-info "boom" {})))
;; => throws ExecutionException wrapping the original at deref timeA thrown exception in a future is held until derefclojure.core/derefDereference a ref/atom/promise/future. Shorthand: @x.view on clojuredocs →, then thrown. This is sometimes called the "thrown-on-realize" trap — silent failure until somebody reads the value.
In tests, the trap bites hardest: a future that throws but is never
derefclojure.core/derefDereference a ref/atom/promise/future. Shorthand: @x.view on clojuredocs →'d passes the test silently. Always derefclojure.core/derefDereference a ref/atom/promise/future. Shorthand: @x.view on clojuredocs → (or wrap in try)
before the test ends.
Real-world
| Pattern | Where |
|---|---|
| Parallel HTTP fan-out for fan-in aggregation | API gateways aggregating microservice responses |
| delayclojure.core/delayLazy computation evaluated at most once on first deref.view on clojuredocs → around a singleton schema/regex | Avoids JVM startup cost when the value isn't always needed |
| promiseclojure.core/promiseA write-once container; readers block on deref until delivered.view on clojuredocs → 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.)