Some work takes a while and mostly waits: a timer, a request, a user's click. An async function can pause at those points and let other code run until the wait is over. This page covers @async and await, running several things at once, errors, and how the waiting is scheduled. The exact rules are on the @async, await and Promise reference cards.
A function marked @async is declared as returning a Promise<Result>, and its returns give a plain Result. With no return type it returns a Promise<Undefined>. This works the same way a generator is declared as returning an Iterator<T> and yields plain Ts. Calling it runs the body at once, up to its first await, and then hands back the promise, a value that isn't ready yet.
await p pauses the async function it's in until p settles, and gives the promise's value. await is only allowed inside an @async body. Because the caller has to be async too to wait this way, async functions tend to call each other, up to an entry point that waits for them (see below).
Calling an async function starts it, and awaiting it only waits for it. To have several running at once, call them all first and await afterwards. Promise.all waits for every one and gives their values in the order they were passed, and Promise.race gives the first one to settle. The others keep running, since nothing cancels a promise.
Awaiting each in turn, let a := await brew("green") and then let b := await brew("black"), would start the second only after the first was done.
A promise has no failed state of its own. It settles with a value, and a function that can fail says so in its result type like any other: Promise<Error ! Settings>. await binds tighter than !! but looser than a call, so await readLater(path)!! awaits the call and then bubbles the error the value carries. In an async body, !! and error end the body the way they end any function, and the promise settles with the errored value. See Errors.
p.timeout(after) gives a promise of TimeoutError ! Type that settles with a TimeoutError if p hasn't settled in time. A match on the awaited value then handles the timeout, the other errors and the value separately.
a.zip(b) waits for two promises of different types and gives both values as a pair. Promise.firstOk is a race that skips errors: it gives the first value that isn't one, or the last error when every promise fails. Promise.allLimited(items, limit, start) works like Promise.all(items.map(start)), but no more than limit run at once. That's useful for a list of requests a server would otherwise get all at the same moment. Promise.retry(attempts, start, wait) starts the work again while it gives an error, waiting wait before the second try and twice as long before each one after. It stops early when &cancellation is cancelled. p.inspect(f) runs f on the value as it passes, for logging, and settles with the same value.
The event loop runs on one thread. Each async call is a task, and the event loop runs one task at a time until it finishes or reaches an await on a promise that isn't settled yet. Tasks don't run in parallel, so two of them can't change the same value at the same moment. The value can still change while a task is paused at an await, so read it again after the await instead of relying on what was read before it.
Code between two awaits runs without a break. A long computation, or a call that blocks such as &clock.sleep or the &fs and &stdin methods, holds up every other task until it returns. Promise.sleep waits without blocking.
Timers go by &clock. When no task is due, the loop calls &clock.sleep until the next one is. With a FixedClock injected, that moves the time forward at once, so async code that waits minutes runs instantly in a test and still sees the time pass. See Context.
A long computation holds up the loop, and it uses one core however many the machine has. A function marked @parallel runs on another thread instead: calling it hands the work to one of the host's threads, one per core, and gives back a Promise at once. Like an @async function it's declared as returning Promise<Result>, and its returns give a Result. To run a function that isn't marked, Promise.parallel(work, ..arguments) takes it by name with its arguments.
The work and the program don't share anything. The arguments are copied to the thread, and the result is copied back when the promise settles, so neither can hold something that only makes sense where it was made: a function, a Promise, an Iterator, a JsValue, a Regex or a Pointer. The work can't await, and an error it ends with settles the promise like any other value. A panic on the thread ends the program, the same as a panic anywhere.
The work also has to be self-contained. It can't read a context value it doesn't inject itself, because the injections of the code that called it stay on the caller's thread. It can't do I/O except through values it injects, and it can't change what it was given. What it does inject stays on its thread. The checker says when work isn't self-contained, and why.
Self-contained work gives the same result wherever it runs. A host without threads runs it at once and gives a settled promise: a WASI runtime or a native executable. On Bun and Node there's one thread per core, and the REFINGERE_THREADS environment variable sets how many (0 runs everything on the main thread). Tests run side by side the same way. See Testing.
A web page built with rfn --web has one Web Worker per core, each with the program's module, and the site gets the worker's script, rfn-thread.js, beside the module. The page's own thread never waits for them: the work's promise settles once a worker's done, and what comes after the await runs then, so the page stays responsive meanwhile. A @js function that starts such work gives back at once - call runAsEntry() on the promise to run what's ready, and hand the result to the page through a @js declaration of its own. Unsafe work, which takes the caller's context values along, runs at once on the page's own thread, and work handed a shared value (below) can't go to a page's workers.
@parallel(unsafe), and Promise.parallelUnsafe, check nothing the work does. It gets copies of the caller's context values, can read and write files and write to the console, and changes what it likes. Getting that right is up to you. What it changes on its own copies stays on its thread. To share, use the types made for it. A SharedMap, a Channel, a SharedCounter or a SharedValue handed to a thread is the same one there, not a copy.
A shared value holds copies too: what goes in is copied, and so is what comes out, so counts.get(key).push(x) changes nothing stored. Set the value back, or use update, which reads, changes and stores it without any other thread getting in between. locked(f) does the same for several steps. A Channel passes values from one thread to another in order. receive() waits for the next one, and gives undefined once the channel's closed and empty; Channel.receiveAny(channels) waits on several at once and says which one a value came from. A SharedValue is a single value with the same update. A shared value can't hold another.
Where there are no threads, unsafe work runs at once on the program's own thread. There, what it changes is the caller's own values, not copies, so a program that relies on the copies behaves differently - one more thing that's yours to get right.
SharedValue.update and SharedMap.update change one value with no other thread in between. When a thread needs several steps to happen together - read two values, write a third - it takes a Mutex: locked(λ ...) runs the work while no other thread holds the lock, waiting its turn. A Semaphore has a number of permits instead of one, so at most that many threads hold one at a time - withPermit limits how much work runs at once, three downloads at a time. Both are shared the way a Channel is: copied into a thread's work, they're still the same lock. Waiting for one asks &cancellation, and a lock held by the thread that's waiting for it is never let go - lock in the same order everywhere.
Work that runs long should stop when nobody wants its result anymore - the user closed the window, the request timed out. Nothing stops it by force. Instead the work asks between its steps whether it's been cancelled, and ends with a CancelledError error when it has:
&cancellation is a context value, so the code that starts the work decides, and nothing in between has to pass it along. By default it's one nothing cancels. A caller injects a Cancellation.new() and calls cancel(reason) later, or injects Cancellation.after(Duration.seconds(30.0)) to give the work a deadline on &clock - a FixedClock moves it in a test. child() makes one that stops when its parent does, and can also be cancelled on its own.
Waiting notices it too. Promise.sleep ends early when the work is cancelled or its deadline comes, so the next check() stops it at once. timeout gives up with a TimeoutError whose cause is the CancelledError error, and Channel.receive stops waiting and gives undefined. onCancel(f) runs your own code when cancel is called - to close a connection, say.
@main, @test and routes can be @async. The program, the test or the request then runs the loop until the promise settles. The test above checks that both cups brewed side by side: under a FixedClock the whole morning takes three minutes of clock time and no real time.
Code that isn't async can call block() on a promise. It runs the event loop, other tasks included, until that promise settles, and gives its value. Don't call it inside an async function. There it would hold up the loop the awaited work runs on, so use await instead.
Promise.withResolver() gives a pending promise and the lambda that settles it, for turning an API that calls back later into one that can be awaited. Only the first value counts: calling the lambda again does nothing.
An async function sees the context of the call that started it, for its whole run, even after the caller's block, and the injects in it, have ended. A defer in an async function runs when the body ends. An await is a pause, not an ending, so it doesn't trigger a defer. A generator can't be @async.
For a longer program, see Concurrent Jobs.