Liking cljdoc? Tell your friends :D

async-style

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.


📖 Table of Contents


🚀 Features

  • Async/Await Syntax: Familiar JavaScript-style asynchronous programming in Clojure.
  • Rich Error Handling: Built-in mechanisms for handling asynchronous exceptions gracefully.
  • Flexible Execution Pools: Optimized for I/O-bound, compute-heavy, and lightweight tasks.
  • Cancellation Support: Easily cancel asynchronous operations, ensuring efficient resource management.
  • Comprehensive Utilities: Timeout, sleep, chaining tasks, racing tasks, and more.

🔥 Quick Start

(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)))

📦 Installation

Leiningen

[com.xadecimal/async-style "0.1.0"]

Clojure CLI (deps.edn)

{:deps {com.xadecimal/async-style {:mvn/version "0.1.0"}}}

Why async-style?

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:

  • Familiar ergonomics: async/await like in JavaScript, Python, C#, etc.
  • Expanded ergonomics: blocking/wait for I/O and compute/wait for heavy compute.
  • Separate pools: dedicated threads for async control flow (async), blocking I/O (blocking) and CPU‑bound work (compute).
  • Built on core.async: core.async under the hood, can be used alongside it, promises are core.async's promise-chan.
  • First-class error handling: unlike core.async, errors are bubbled up and properly handled.
  • First‑class cancellation: propagate cancellation through promise‑chans.
  • Rich composition: then, chain, race, all, any, all-settled.
  • Convenient macros: ado, alet, clet for sequential, ordered, or concurrent binding.
  • Railway programming: supports railway programming with async/await*, if preferred.

Core Concepts

Pools: async, blocking, compute

async-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.

When used with core.async <= 1.7

MacroExecutor poolUse for…
asynccore.async’s go‑dispatch (8 threads)async control flow
blockingunbounded cached threadsblocking I/O, sleeps
computefixed agent pool (cores + 2 threads)CPU‑intensive work, do not block

When used with core.async >= 1.8

MacroExecutor poolUse for…
asyncunbounded cached threadsasync control flow
blockingunbounded cached threadsblocking I/O, sleeps
computefixed agent pool (cores + 2 threads)CPU‑intensive work, do not block

When used with core.async >= 1.8 with virtual threads

MacroExecutor poolUse for…
asyncvirtual thread executorasync control flow
blockingvirtual thread executorblocking I/O, sleeps
computefixed agent pool (cores + 2 threads)CPU‑intensive work, do not block

Awaiting: await, wait, await*, wait*

  • await (inside async): parks current go‑thread until a promise‑chan 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.

Errors

Implicit try / catch / finally

All of async, blocking, compute and await:

  1. Automatically wrap your body in a single try if you include any trailing (catch …) or (finally …) forms.
  2. Pull in any (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")))
  • Missing catch/finally? No wrapping is done (no overhead).
  • Only catch or only finally? Works the same.
  • Multiple catch? All are honored in order.

This gives you JS‑style inline error handling right in your async blocks.

Error Handling Combinators

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))
    

Railway style with 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

Cancellation in async-style is cooperative—you signal it with cancel!, but your code must check for it to actually stop.

  • 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.

  • 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).
(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!")))

API Overview

Task Creation

Function / MacroDescription
asyncRun code on the async‑pool
blockingRun code on blocking‑pool
computeRun code on compute‑pool
sleepasync sleep for ms
deferdelay execution by ms then fulfill value or fn

Chaining & Composition

FunctionDescription
await / waitRetrieve result (throws on error)
await* / wait*Retrieve result or exception as a value (railway style)
thenRun callback on success (skips on error)
chainThread multiple then calls (short‑circuits on first error)
catchRecover from errors of a given type or predicate
finallyAlways run side‑effect on success or error, then re‑deliver
handleRun handler on outcome (error or success) and deliver its return
error? / ok?Predicates to distinguish exception values from normal results

Timeouts & Delays

FunctionDescription
sleepAsynchronously pause for ms milliseconds; returns a promise‑chan that fulfills with nil after the delay.
deferWait ms milliseconds, then asynchronously deliver a given value or call a provided fn; returns a promise‑chan.
timeoutWrap a channel with a ms deadline—if it doesn’t fulfill in time, cancels the original and delivers a TimeoutException (or your custom fallback).

Racing & Gathering

FunctionDescription
raceFirst chan (or error) to complete “wins”; cancels all others
anyFirst successful chan; ignores errors; errors aggregated if all fail
allWait for all; short‑circuits on first error; returns vector of results
all-settledWait for all, return vector of all results or errors

Helpers: ado, alet, clet, time

MacroDescription
adoAsynchronous do: execute expressions one after another, awaiting each; returns a promise‑chan of the last expression.
aletAsynchronous let: like let but each binding is awaited in order before evaluating the body.
cletConcurrent let: evaluates all bindings in parallel, auto‑awaiting any dependencies between them.
timeMeasure wall‑clock time of a sync or async expression; prints the elapsed ms and returns the expression’s value.

Asynchronous let (alet) and Concurrent let (clet)

Bind async expressions just like a normal let—but choose between sequential or parallel evaluation:

MacroSemantics
aletSequential: awaits each binding in order before evaluating the next.
cletConcurrent: 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)
  • Use alet when bindings must run in sequence.
  • Use clet to maximize concurrency, with dependencies automatically respected.

📖 Contributing

Contributions, issues, and feature requests are welcome! Feel free to submit a pull request or open an issue on GitHub.


📃 License

Distributed under the MIT License. See LICENSE for more details.


❤️ Support

If you find async-style useful, star ⭐️ the project and share it with the community!

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close