Concurrency
Cooperative async concurrency with promises and channels. Tasks run on the VM's cooperative scheduler, interleaving at yield points (channel operations, await, sleep).
Async features require the VM backend (default since v1.13). The tree-walker returns an error.
Scheduling guarantees
- Spawn order is preserved. When several tasks are simultaneously ready to run, the scheduler picks them in the order they were spawned. A pipeline of
(async (send-1)) (async (send-2)) (async (send-3))followed by sequential receives yields1 2 3, not a reordered surface. - Wake order is FIFO. When a value becomes available on a channel, the longest-waiting receiver is woken first.
- Cooperation, not parallelism. Tasks interleave at yield points (channel ops,
await,sleep). CPU-bound tasks without yield points run to completion before other tasks get a turn.
Promises
async/spawn
(async/spawn thunk) → async-promiseSpawn a zero-argument function as an async task. Returns a promise that resolves when the task completes.
(define p (async/spawn (fn () (+ 1 2))))
(async/await p) ; => 3Usually called via the async special form:
(define p (async (+ 1 2)))
(await p) ; => 3async/await
(async/await promise) → valueWait for a promise to resolve. Inside an async task, yields to the scheduler. At the top level, runs the scheduler inline until the promise resolves. Raises an error if the promise was rejected.
async/all
(async/all promises) → listRun all promises to completion and return a list of their results. Takes a list or vector of promises.
(let ((p1 (async 10))
(p2 (async 20))
(p3 (async 30)))
(async/all (list p1 p2 p3))) ; => (10 20 30)async/race
(async/race promises) → valueReturn the value of the first promise to resolve. Takes a list or vector of promises.
async/resolved
(async/resolved value) → async-promiseCreate an already-resolved promise wrapping value.
async/rejected
(async/rejected message) → async-promiseCreate an already-rejected promise with message.
async/run
(async/run)Run all pending async tasks to completion.
async/sleep
(async/sleep ms)Inside an async task, yield for at least ms milliseconds (real timing — the scheduler will not wake the task until the deadline elapses). Outside async, calls thread::sleep.
async/timeout
(async/timeout ms promise) → valueWait for promise to resolve, but raise an error if it takes longer than ms milliseconds. The underlying task is not automatically cancelled; pair with async/cancel if you need to free its resources.
(async/timeout 100 (async (do-slow-work)))
;; raises: async/timeout: operation timed outms = 0 causes an immediate timeout if the promise has not already resolved.
async/cancel
(async/cancel promise) → boolRequest cancellation of a spawned task. Returns #t if the call actually transitioned the promise into the Cancelled state, #f if there was nothing to cancel — the promise was already terminal (resolved, rejected, previously cancelled) or was never spawned in the first place (e.g. created via async/resolved).
Cancellation is best-effort and never errors. The next time the task hits a yield point, it transitions to Cancelled; subsequent (await p) raises "async/await: task was cancelled" (distinct from a normal rejection).
(async/cancel (async/resolved 1)) ;; => #f (never spawned)
(let ((p (async 42))) (await p) (async/cancel p)) ;; => #f (already resolved)
(let ((p (async (async/sleep 100)))) (async/cancel p)) ;; => #tasync/cancelled?
(async/cancelled? promise) → bool#t if promise is in the Cancelled state — distinct from async/rejected?. Matches the state variant directly rather than the rejection message, so a user (async/rejected "cancelled") no longer aliases:
(async/cancelled? (async/rejected "cancelled")) ;; => #fPromise predicates
The four predicates partition the terminal states: a promise is at most one of resolved / rejected / cancelled, and pending? is the complement of those three.
| Function | Description |
|---|---|
(async/promise? x) | Is x an async promise? |
(async/resolved? p) | Is promise p resolved? |
(async/rejected? p) | Is promise p rejected? (excludes cancelled) |
(async/pending? p) | Is promise p still pending? |
(async/cancelled? p) | Was promise p cancelled? |
Channels
Bounded FIFO channels for communication between async tasks.
channel/new
(channel/new) → channel ; capacity 1
(channel/new capacity) → channelCreate a bounded channel. Default capacity is 1. Capacity must be at least 1.
channel/send
(channel/send ch value)Send a value to the channel. If the channel is full and inside an async task, yields until space is available. Outside async context, raises an error if full. Raises an error if the channel is closed.
channel/recv
(channel/recv ch) → valueReceive a value from the channel. If the channel is empty and inside an async task, yields until data is available. Outside async context, raises an error if empty. Returns nil if the channel is closed and empty.
channel/try-recv
(channel/try-recv ch) → value | nilNon-blocking receive. Returns the next value or nil if the channel is empty.
channel/close
(channel/close ch)Close the channel. Subsequent sends will error. Blocked receivers will wake with nil.
Channel predicates
| Function | Description |
|---|---|
(channel? x) | Is x a channel? |
(channel/closed? ch) | Is the channel closed? |
(channel/empty? ch) | Is the channel buffer empty? |
(channel/full? ch) | Is the channel buffer at capacity? |
(channel/count ch) | Number of values in the buffer |
Examples
Producer/Consumer
(let ((ch (channel/new 1)))
(let ((producer (async
(channel/send ch 10)
(channel/send ch 20)
(channel/send ch 30)
(channel/close ch)))
(consumer (async
(let loop ((sum 0))
(let ((val (channel/recv ch)))
(if (nil? val)
sum
(loop (+ sum val))))))))
(await consumer))) ; => 60Parallel computation
(let ((p1 (async (fib 30)))
(p2 (async (fib 31))))
(+ (await p1) (await p2)))See Scheduling guarantees above for the full ordering / cooperation rules.
Async ops inside higher-order functions
Stdlib higher-order functions like for-each, map, filter, foldl, sort-by, apply, reduce, partition, any, every can call lambdas that perform async operations (channel/send, channel/recv, await, async/sleep). The yield suspends inside the callback and resumes correctly:
(let ((ch (channel/new 3)))
(let ((producer (async
(for-each (fn (n) (channel/send ch n))
(list 1 2 3 4 5 6 7))
(channel/close ch)))
(consumer (async
(let loop ((sum 0))
(let ((v (channel/recv ch)))
(if (nil? v) sum (loop (+ sum v))))))))
(await consumer))) ;; => 28Yielding native functions (e.g., channel/recv, async/sleep) passed directly as the callback produce a clear error pointing to the workaround:
;; Error: yielding native passed directly to a higher-order function — wrap in a lambda
(map channel/recv (list ch ch ch))
;; Correct: wrap the native in a lambda
(map (fn (c) (channel/recv c)) (list ch ch ch))