async / await for Clojure.
A Clojure library that brings JavaScript's intuitive async/await and Promise APIs to Clojure, along with utilities for cancellation, timeouts, racing, and more. It brings familiar async/await style of programming to Clojure.
adoseq, afor, areduce, atransduce, and ainto.(ns example.core
(:require [com.xadecimal.async-style :as a
:refer [async blocking compute await wait cancel! cancelled?]]))
;; Simple async operation
(async
(println "Sum:" (await (async (+ 1 2 3)))))
;; Blocking I/O, asynchronous waiting
(async
(println (await (blocking
(Thread/sleep 500)
"Blocking completed!"))))
;; Blocking I/O, synchronous waiting
(println "Sync blocking result:"
(wait (blocking
(Thread/sleep 500)
"Blocking sync done")))
;; Heavy compute, asynchronous waiting
(async
(println "Factorial:"
(await (compute (reduce * (range 1 20))))))
;; Heavy compute, synchronous waiting
(println "Sync compute factorial:"
(wait (compute (reduce * (range 1 20)))))
;; Handle an error
(async
(println "Divide:" (await (async (/ 1 0))))
(catch ArithmeticException _
(println "Can't divide by zero!")))
;; Cancel an operation
(let [task (blocking (Thread/sleep 5000)
(when-not (cancelled?)
(println "This won't print")))]
(Thread/sleep 1000)
(cancel! task "Cancelled!")
(println "Task result:" (wait task)))
[com.xadecimal/async-style "0.2.0"]
{:deps {com.xadecimal/async-style {:mvn/version "0.2.0"}}}
Core.async and the CSP style is powerful, but its go blocks and channels can feel low‑level compared to JS Promises and async/await. async-style provides:
async/await like in JavaScript, Python, C#, etc.blocking/wait for I/O and compute/wait for heavy compute.async), blocking I/O (blocking) and CPU‑bound work (compute).promise-chan.Future, CompletableFuture, and IBlockingDeref can be observed through ->promise-chan.then, chain, race, all, any, all-settled.ado, alet, clet for sequential, ordered, or concurrent binding.async/await*, if preferred.async, blocking, computeasync-style follows the best practice outlined here: Best practice for async/blocking/compute pools
Meaning it offers async/blocking/compute, each are meant to specialize the work you will be doing so they get executed on a pool that is most optimal.
| Macro | Executor pool | Use for… |
|---|---|---|
async | core.async’s go‑dispatch (8 threads) | async control flow |
blocking | unbounded cached threads | blocking I/O, sleeps |
compute | fixed agent pool (cores + 2 threads) | CPU‑intensive work, do not block |
| Macro | Executor pool | Use for… |
|---|---|---|
async | unbounded cached threads | async control flow |
blocking | unbounded cached threads | blocking I/O, sleeps |
compute | fixed agent pool (cores + 2 threads) | CPU‑intensive work, do not block |
| Macro | Executor pool | Use for… |
|---|---|---|
async | virtual thread executor | async control flow |
blocking | virtual thread executor | blocking I/O, sleeps |
compute | fixed agent pool (cores + 2 threads) | CPU‑intensive work, do not block |
await, wait, await*, wait*await (inside async): parks current async execution until a promise‑chan or supported async value completes; re‑throws errors.wait (outside async): blocks calling thread for a result or throws error.await* / wait*: return exceptions as values (no throw).await is like your JavaScript await, but just like core.async's go, it cannot park across function boundaries,
so you have to be careful when you use macros like map or run!, since those use higher-order functions,
you cannot await with them. This is true in JavaScript's await as well, but it's less common to use
higher-order functions in JS, so it doesn't feel as restrictive. Once core.async support for virtual thread is added, and if you run under a JDK that supports them, this limitation will go away.
wait is the counter-part to await, it is like await but synchronous. It doesn't color your functions, but will block your thread.
await* / wait* are variants that return the exception as a value, instead of throwing.
await and wait accept core.async channels, async-style promise-chans, plain values, and values supported by IntoPromiseChan.
They explicitly take from channel-like inputs. If a producer settled to a source channel as its value, await / wait of that producer returns the source channel itself.
->promise-chan converts supported asynchronous values into promise-chans:
Future, CompletableFuture, and other IBlockingDeref values are observed from a detached blocking task.Future / CompletableFuture when that is supported.Most async-style APIs call ->promise-chan for their inputs, so helpers such as await, wait, timeout, race, any, all, and all-settled can observe futures as well as promise-chans.
When async, blocking, or compute returns a promise-like single-result async value, async-style waits one level before settling the producer's result channel.
This keeps the producer's lifetime aligned with returned promise-like values:
(wait (async (async (async 1)))) ; => 1
Nested async-style producers still compose because each producer waits one promise-like level before it settles. Multi-value source channels are different: returning an async-generator or ordinary raw channel settles to the channel itself. The source is not consumed by settlement, and a cold generator's producer starts only when that returned channel is later consumed.
async-generator is the many-value counterpart to async. It returns a cold core.async-compatible channel-like value. The generator body starts when the returned channel is consumed, not when the generator is created.
(defn ticks []
(async-generator
(yield "starting")
(await (sleep 100))
(yield "middle")
(await (sleep 100))
(yield "done")
(finally
(println "cleanup"))))
(async
(adoseq [tick (ticks)
:while (not= tick "done")]
(println tick)))
Values yielded by yield are raw channel values. Channel close means the generator is done. yield applies async-style value semantics before publishing, so yielding (async 1), a Future, or a CompletableFuture exposes the settled value to consumers. Because async-style stays channel-first, generators cannot yield nil; a nil take means done.
If the generator body fails, its Throwable is delivered through the output channel before that channel closes. Await-aware consumers rethrow it using normal async-style error semantics, and generator finally cleanup still runs. Lifecycle cancellation and areturn cleanup do not publish their internal interruption signals as source values.
The async iteration consumers are:
anext: manually take one raw value; settles to nil when done.areturn: manually request early cleanup and wait for generator finally.adoseq: async doseq for side effects; settles to nil.afor: async eager for; settles to a vector of body results.areduce: async reduce; (reduced v) short-circuits and settles to v.atransduce: async transduce with lifecycle-aware early cleanup.ainto: async into, optionally with a transducer.adoseq and afor support destructuring, nested bindings, :let, :when, and :while. Async iteration consumers can consume channel-like sources or collection-like sources, including Clojure seqables, JVM arrays, and java.lang.Iterable values. Collection elements are awaited, so this works:
(wait (afor [x [(async 1) (future 2) 3]
:let [y (* x 2)]]
y))
;; => [2 4 6]
Manual stepping is available when a caller wants explicit lifecycle control:
(let [source (ticks)]
(println (wait (anext source)))
(println (wait (anext source)))
(wait (areturn source))) ; runs generator finally
anext only supports channel-like sources for now. It starts a cold generator if needed, observes borrowed plain channels without closing or cancelling them, and returns a promise-chan that settles to the next raw value or nil when the source is done. It does not return a JS-style {done?, value} map. areturn is a no-op for borrowed plain channels.
For async generators, (cancel! source) is lifecycle-aware fire-and-forget cancellation: it closes the output channel, unblocks paused yields, cancels the producer if it started, and lets generator catch / finally run. Use areturn when you need to wait until finally / finalization has completed. close! on a generator is only the lower-level raw channel close operation, not the lifecycle cleanup API.
Async generators use fixed, lossless buffering:
(async-generator
{:buffer-size 32}
(yield :event))
The default buffer size is 0, which means an unbuffered, JS-like pull handoff: after a consumer takes a yielded value, yield does not return and code after it does not run until the consumer asks for another value or calls areturn. Use a positive :buffer-size when you want bounded runahead. Dropping and sliding buffers are intentionally not part of async-generator, because a generator models a lossless sequence. Use an explicit core.async channel adapter for push/event sources that need lossy buffering.
Async iteration follows the same ownership rule as the rest of async-style. Creating or returning a cold generator does not start work. Consuming it starts its producer in the current scope, making that producer an owned child of the consuming scope. Early areturn, adoseq, afor, areduce, atransduce, or ainto exit calls generator cleanup and waits for finally to run. Borrowed plain channels are only observed; they are not closed or cancelled.
Reducing functions and transducer steps passed to areduce, atransduce, and ainto run inside async-pool orchestration code. Keep them quick and synchronous. Do not block, park, await, wait, perform I/O, or do heavy compute there; wrap that work in blocking, compute, adoseq, afor, or an async-generator first.
try / catch / finallyAll of async, blocking, compute and await:
try if you include any trailing
(catch …) or (finally …) forms.(catch Type e …) or (finally …) at the end of your block
into that try, so you don’t have to write it yourself.;; without implicit try, you’d need:
(async
(try
(/ 1 0)
(catch ArithmeticException e
(println "oops:" e))
(finally
(println "done"))))
;; with implicit try, just append catch/finally:
(async
(/ 1 0)
(catch ArithmeticException e
(println "oops:" e))
(finally
(println "done")))
This gives you JS‑style inline error handling right in your async blocks.
async-style gives you first‑class combinators to catch, recover, inspect or always run cleanup on errors:
catch
Intercept errors of a given type or predicate, return a fallback value:
;; recover ArithmeticException to 0
(-> (async (/ 1 0))
(catch ArithmeticException (fn [_] 0)))
finally
Always run a side‑effect (cleanup, logging) on both success or error, then re‑deliver the original result or exception:
(-> (async (/ 1 0))
(finally (fn [v] (println "Completed with" v))))
handle
Always invoke a single handler on the outcome (error or value) and deliver its return:
;; log & wrap both success and error
(-> (async (/ 1 0))
(handle (fn [v] (str "Result:" v))))
then
Attach a success callback that is skipped if an error occurred (short‑circuits on error):
(-> (async 5)
(then #(+ % 3))
(then println))
chain
Shorthand for threading multiple then calls, with the same short‑circuit behavior:
(-> (async 5)
(chain inc #(* 2 %) dec)
(handle println))
await* / wait*By default await/wait will throw any exception taken from a chan. If you prefer railway programming (errors as values), use:
await* (parking, non‑throwing)wait* (blocking, non‑throwing)They return either the successful value or the Throwable. Functions error? and ok? can than be used to branch on their result:
(async
(let [result (await* (async (/ 1 0)))]
(if (error? result)
(println "Handled error:" (ex-message result))
(println "Success:" result))))
Errors are always modeled this way in async-style, unlike in JS which wraps errors in a map, in async-style they are either an error? or an ok? result:
(async
(let [vals (await* (all-settled [(async 1) (async (/ 1 0)) (async 3)]))]
(println
(map (fn [v] (if (error? v) :err v)) vals))))
;; ⇒ (1 :err 3)
Cancellation in async-style is cooperative—you signal it with cancel!, but your code must check for it to actually stop.
Cancellation follows ownership, not observation:
async, blocking, or compute inside a running execution creates a direct owned child, unless the start happens inside detach.race, any, all, all-settled, and timeout do not take ownership of borrowed input promises/channels.race and any do not cancel losers because they lost. Locally started losers are only cancelled if their owning parent scope ends while they are still unfinished.detach when intentionally starting background work that should outlive the current parent scope.Each async, blocking, and compute execution creates a cancellation scope. While the body runs, that execution's own result promise-chan is bound as the current cancellation token. Work started inside that scope becomes a direct owned child unless it is started inside detach.
Owned children are cancelled when:
Only direct children are registered on a parent. Transitive cancellation happens recursively: the parent cancels its direct children, each child cancels its direct children, and so on.
Observation does not create ownership. Passing an already-started promise, channel, Future, or CompletableFuture to await, wait, timeout, race, any, all, or all-settled observes it without making it a child of the current scope. Internal watcher logic, when used, belongs to the current operation but does not transfer ownership of the watched input.
Producer settlement and scope lifetime are tied together. If a producer returns a promise-like single-result async value, the producer waits one level for that value before settling its own result channel. Cleanup of unfinished owned children happens only after the producer's result channel is actually settled. Nested async-style producers compose because each producer performs this one-level wait. Multi-value source channels, including async generators and ordinary raw channels, are preserved as values; first consumption owns any cold generator producer.
Borrowed work keeps its original owner:
(let [p1 (async slow)
p2 (async fast)]
(async
(race [p1 p2]))) ; observes p1/p2; does not own or cancel them
Locally started work is owned by the surrounding scope:
(async
(race [(async slow)
(async fast)]))
;; race observes both; when the parent scope settles, any unfinished owned child is cancelled.
The same distinction applies when a combinator is returned from an async body:
;; Borrowed inputs: the inner async only observes p1/p2.
(let [p1 (async slow)
p2 (async fast)]
(async
(race [p1 p2])))
;; If the inner async is cancelled, only its waiting/race logic is cancelled.
;; p1 and p2 keep running under their original owner.
;; Locally started inputs: slow and fast are owned by the outer async.
(async
(race [(async slow)
(async fast)]))
;; race itself does not cancel the loser. But after race settles the parent
;; async settles too, and parent cleanup cancels any unfinished owned child.
;; Explicit background work: detach removes the ownership edge.
(async
(detach
(async slow-background-work))
:parent-done)
;; The detached child may continue after the parent completes or is cancelled.
Returning a combinator does not make the combinator own borrowed inputs. It only keeps the parent producer alive until the returned combinator's promise-chan settles. After that, normal parent cleanup applies to unfinished children that were actually started inside the parent scope.
cancel!
(cancel! ch) ; cancel with CancellationException
(cancel! ch custom-val) ; cancel with custom-val (must not be nil)
Marks the promise‑chan ch as cancelled. If the block hasn’t started, it immediately fulfills; otherwise it waits for you to check. On lifecycle-aware async-style sources such as async generators, cancel! requests source cleanup and returns immediately; use areturn to wait for cleanup.
cancelled?
(when-not (cancelled?) …)
Returns true inside async/blocking/compute if cancel! was called. Does not throw.
check-cancelled!
(check-cancelled!) ; throws InterruptedException if cancelled
Throws immediately when cancelled, so you can use it at safe points to short‑circuit heavy loops or I/O.
Handling cancellation downstream
await / wait will re‑throw a CancellationException (or return your custom val).detach
(async
(detach
(async background-work)))
Runs the body outside the current cancellation context. Work started inside detach is not owned by the current parent and can outlive parent cancellation, failure, or normal completion. detach can also be used around await / wait when you intentionally do not want to read the current parent's cancellation token.
(def work
(async
(loop [i 0]
(check-cancelled!)
(println "step" i)
(recur (inc i)))))
;; let it run a bit…
(Thread/sleep 50)
(cancel! work)
;; downstream:
(async
(await work)
(catch CancellationException _
(println "Work was cancelled!")))
| Function / Macro | Description |
|---|---|
async | Run code on the async‑pool |
blocking | Run code on blocking‑pool |
compute | Run code on compute‑pool |
detach | Start work outside the current ownership scope |
async-generator | Create a cold channel-like async source |
yield | Publish one value from inside async-generator |
->promise-chan | Coerce supported async values to promise‑chans |
sleep | async sleep for ms |
defer | delay execution by ms then fulfill value or fn |
| Function | Description |
|---|---|
await / wait | Retrieve result (throws on error) |
await* / wait* | Retrieve result or exception as a value (railway style) |
then | Run callback on success (skips on error) |
chain | Thread multiple then calls (short‑circuits on first error) |
catch | Recover from errors of a given type or predicate |
finally | Always run side‑effect on success or error, then re‑deliver |
handle | Run handler on outcome (error or success) and deliver its return |
error? / ok? | Predicates to distinguish exception values from normal results |
| Function | Description |
|---|---|
sleep | Asynchronously pause for ms milliseconds; returns a promise‑chan that fulfills with nil after the delay. |
defer | Wait ms milliseconds, then asynchronously deliver a given value or call a provided fn; returns a promise‑chan. |
timeout | Observe a channel with a ms deadline; returns its result or a TimeoutException/custom fallback without cancelling the input. |
| Function | Description |
|---|---|
race | First input value or error to settle wins; observes inputs without cancelling losers |
any | First successful input wins; ignores errors until all fail, then returns an aggregate error |
all | Wait for all inputs; short‑circuits on first error without cancelling borrowed inputs |
all-settled | Wait for all inputs and return a vector of values and errors without short‑circuiting |
Notes:
all and any short-circuit their returned promise-chan, but that is observation only. They do not cancel borrowed unfinished inputs.any errors, it settles with an ex-info whose data includes {:type :all-errored, :errors [...]}.| Function / Macro | Description |
|---|---|
async-generator | Cold async source that yields many raw values over a core.async channel |
yield | Publish one settled async-style value from inside an async generator |
anext | Take one raw value from a channel-like source, or nil when done |
areturn | Request lifecycle-aware source cleanup and wait for finalization |
adoseq | Async doseq; consumes sources for side effects and settles to nil |
afor | Async eager for; collects body results into a vector |
areduce | Async reduce with lifecycle-aware early cleanup |
atransduce | Async transduce over a channel-like or collection-like source |
ainto | Async into, with optional transducer support |
ado, alet, clet, time| Macro | Description |
|---|---|
ado | Asynchronous do: execute expressions one after another, awaiting each; returns a promise‑chan of the last expression. |
alet | Asynchronous let: like let but each binding is awaited in order before evaluating the body. |
clet | Concurrent let: evaluates all bindings in parallel, auto‑awaiting any dependencies between them. |
time | Measure wall‑clock time of a sync or async expression; prints the elapsed ms and returns the expression’s value. |
let (alet) and Concurrent let (clet)Bind async expressions just like a normal let—but choose between sequential or parallel evaluation:
| Macro | Semantics |
|---|---|
alet | Sequential: awaits each binding in order before evaluating the next. |
clet | Concurrent: starts all bindings in parallel, auto‑awaiting any that depend on previous ones while letting independent bindings overlap. |
alet example(alet
[a (defer 100 1)
b (defer 100 2)
c (defer 100 3)]
(println "Sum =" (+ a b c)))
;; Prints "Sum = 6" after ~300 ms
clet example(clet
[a (defer 100 1)
b (defer a 2) ;; waits for `a`
c (defer 100 3)] ;; independent of `a` and `b`
(println "Sum =" (+ a b c)))
;; Prints "Sum = 6" after ~200 ms (a and c run in parallel; b waits on a)
alet when bindings must run in sequence.clet to maximize concurrency, with dependencies automatically respected.Contributions, issues, and feature requests are welcome! Feel free to submit a pull request or open an issue on GitHub.
Distributed under the MIT License. See LICENSE for more details.
If you find async-style useful, star ⭐️ the project and share it with the community!
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |