~ / track B / clojure advanced

Futures, promises, and delay

Intermediate

Beyond 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

PrimitiveWhen does it compute?Who computes?Can it be set?
delayFirst @ callThe dereffer's threadNo — captured at construction
futureImmediately on constructionA background thread (in clojure.core/agent-pool)No — captured at construction
promiseNever automaticallyA 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

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

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

PatternWhere
Parallel HTTP fan-out for fan-in aggregationAPI gateways aggregating microservice responses
delay around a singleton schema/regexAvoids JVM startup cost when the value isn't always needed
promise as a bridge between callback and blocking APIsAdapting 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 checksHealth-check endpoints that report "warmup is/isn't done"
Test mocks: (deliver p :ok) from inside a fixtureCoordinating "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.)

 status: new