# Async Elements propagates `async` and `await` up the call stack for the functions that support it: `sql`, `tx`, the others listed below, and calls to your RPC functions. You write those calls without `await`, the compiler awaits them, and every function that calls one becomes `async`. Everything else is ordinary TypeScript, and you write `async` and `await` for it as usual. Exactly which calls are awaited is decided by the `@redirect` build tag. `sql` is a synchronous function whose `@redirect` tag points to `sqlAsync`, an async function. A call to a synchronous function that redirects to an async function is rewritten to `await` the async function, the function making the call becomes `async`, and so does every function that calls it, up the call stack. A `@redirect` to a function that is not async only changes which function is called, and nothing is awaited. ## Why Requiring `await` in front of every call is a leaky abstraction. In most languages you call a function and get its result, and the library handles its own concurrency underneath, with threads or otherwise. `await` at every call site pushes that implementation detail into your code, and into every function above it. Elements inverts it. You call `sql()` and get the result, which is what you expect. Under the hood the compiler still emits a real `await`, so Node.js handles the I/O concurrently and nothing blocks while the query runs. When you want to run several things at the same time, you say so by calling the async variant, such as `sqlAsync()`, and passing the promises to `Promise.all`. ## At a Glance You write this: ```ts // app/lib/users.ts import { sql } from "@elements/app"; export function findUser(id: string) { return sql(` select * from users where id = $1 `, [id]).firstOrThrow("no user"); } export function greet(id: string) { let user = findUser(id); return `hello ${user.name}`; } ``` The compiler emits this: ```js Compiled output async function findUser(id) { return await sqlAsync(...).firstOrThrow("no user"); } async function greet(id) { let user = await findUser(id); return `hello ${user.name}`; } ``` `findUser` became async because `sql` redirects to `sqlAsync`, which is async. `greet` became async because it calls `findUser`. ## How the Build Tag Works Here is the redirect tag on `sql`, in `@elements/app`: ```ts Redirect tag /** * @redirect to=sqlAsync */ declare function sql(text: string, args?: unknown[]): SqlResult; declare function sqlAsync(text: string, args?: unknown[]): Promise>; ``` `@redirect to=sqlAsync` says: when someone calls `sql`, call `sqlAsync` instead. Because `sqlAsync` is async, declared `async` or returning a `Promise`, the call becomes `await sqlAsync(...)` and its caller becomes `async`. The synchronous `sql` is the signature you write against. `sqlAsync` is what actually runs. ## How To Tell Which Functions Participate A call is awaited for you if, and only if, the function you call carries a `@redirect` tag and the function the tag names is async: declared `async`, or returning a `Promise`. Calls to your own RPC functions are awaited too (see below), and so is any function that becomes `async` because it makes one of these calls. A function you marked `async` yourself never participates: see "A function you marked async yourself" below. The tag ships in the package's type declarations, so you never have to guess: ```bash Terminal grep -B3 "@redirect" node_modules/@elements/app/server/index.d.ts ``` Your editor shows the same thing: hover the function and the JSDoc includes the tag. If the declaration has a `@redirect` to an async function, the compiler awaits the call. If it does not, the function is ordinary TypeScript and the usual rules apply. These are the functions in `@elements/app` that redirect to an async function today. Each has a counterpart with the same name and an `Async` suffix, except where noted. Database, from `@elements/app`: ```text Database sql tx checkoutDb ``` Methods on a `Database` or `DatabasePool`: ```text Database methods connect end checkout begin commit rollback sql tx listen notify ``` Channels, sessions, email, jobs, files: ```text Channels, sessions, jobs channel.listen channel.notify listener.stop session.login session.logout session.renew session.revoke session.findActiveSessions email email.send job.schedule Job.cancel File.read sleep ``` App lifecycle: `app.start`, `app.stop`, `client.start`, `client.call`. LiveTable, on the server and in the browser: ```text LiveTable view insert update delete more ``` Tests: ```ts Test test("creates a user", () => { let user = sql(` insert into users ( name ) values ( 'alice' ) returning * `).firstOrThrow(); equal(user.name, "alice"); }); ``` `test` carries a redirect like everything else, so a test body that queries is awaited correctly without you marking it async. It is the one redirected function with no `*Async` counterpart you can call: its promise belongs to the test runner. See `man tests`. RPC functions are a special case. A function tagged `@rpc` has no `@redirect` and no `*Async` name, but it becomes async on both sides, because calling it from the browser is a network round trip: ```ts RPC function /** @rpc */ export function createPost(title: string): Post { return sql(` insert into posts ( title ) values ( $1 ) returning * `, [title]).firstOrThrow(); } ``` The browser calls `createPost(title)` and the compiler awaits it there. See `man rpc`. ## Which Name To Write Two rules decide it, and neither needs judgement. **For a redirected function, write the name that carries the tag.** `sql`, not `sqlAsync`. The compiler inserts the `await`. Never write `await` on a call to a redirected function yourself. If you do, you get "'await' expressions are only allowed within async functions", and the fix is to delete the `await`, not to mark the function `async`. **Write the `*Async` name only when the promise has to be a value in your code**: passed to `Promise.all` or `Promise.race`, stored in a variable you await later, or returned without awaiting. If the word `Promise` does not appear and you are not holding an unawaited result, you want the tagged name. Everything else is plain TypeScript. A function you marked `async` yourself, a promise-returning library call, a `fetch`: you write `async` and `await` for those exactly as you would anywhere else. An `async` body does not change the answer. Marking your function `async`, or using `await` on something else inside it, never means you should switch to an `*Async` name. The suffix is about whether you need the promise, not about what is inside your function. ```ts Choosing a name let user = findUser(id); // tagged: you want the value let [a, b] = await Promise.all([ // Async: you want the promises sqlAsync(`select * from posts`), sqlAsync(`select * from comments`), ]); ``` ## Calling the Async Version Yourself Every redirected function has its `*Async` counterpart exported, with one exception: `test`. Call the counterpart directly when you want control, for example to run queries concurrently: ```ts Concurrent queries export async function loadDashboard(userId: string) { let [posts, comments] = await Promise.all([ sqlAsync(` select * from posts where user_id = $1 `, [userId]), sqlAsync(` select * from comments where user_id = $1 `, [userId]), ]); return { posts, comments }; } ``` When you write `async` and `await` yourself, the compiler leaves those calls alone. It still converts tagged calls elsewhere in the same function, so a body may mix both styles. Concurrency needs its own connection. Two `sqlAsync` calls in a `Promise.all` run concurrently only if they can each take a connection from the pool. Inside a transaction or a test there is a single connection and Postgres serialises on it, so the same two queries take as long as writing them one after the other. ## Where a Declared Type Is the Contract The compiler awaits a call when it can see the body it resolves to. Where a function's value flows somewhere with a declared type of its own, that type is the contract, and the compiler keeps it as written, so each file's output depends only on the file itself. The build reports each of these cases with the change to make. ### A callback declared without a Promise ```ts Wrong function expectError(desc: string, fn: () => void) { try { fn(); errorf("expected %v to fail", desc); } catch (e) { assert(e instanceof ValidationError); } } expectError("a blank name", () => { signup({ name: " " }); // signup queries, so this callback is async }); ``` The callback is async, so `fn()` returns a promise instead of throwing. The `catch` never runs and `errorf` fires every time. The compiler reports the argument, and you widen the parameter: ```ts Widened parameter function expectError(desc: string, fn: () => void | Promise) { ``` The same rule applies wherever a converted function lands: a property in an options object, a class field, an annotated variable, a template attribute. ```ts Other slots function retry(opts: { run: () => void }) { ... } retry({ run: () => { sql(`...`); } }); // reported: widen `run` class W { onDone: () => void; } w.onDone = () => { sql(`...`); }; // reported: widen `onDone` ``` Widening states that the slot accepts an async callback. It does not make the compiler await it for you, and widening alone leaves a `catch` that still cannot run. Mark the function `async` and await the call: ```ts Right async function expectError(desc: string, fn: () => void | Promise) { try { await fn(); errorf("expected %v to fail", desc); } catch (e) { assert(e instanceof ValidationError); } } ``` Calls to a function you marked `async` are not awaited for you, so update each caller too: ```ts Updated caller test("a blank name is refused", async () => { await expectError("a blank name", () => signup({ name: " " })); }); ``` Widening without awaiting is reported when the call sits in a `try`, so you will not ship the half-edit. The same holds for a callback typed `() => unknown`, `() => any` or `() => Promise`: its type allows a promise, so an unawaited call in a `try` is reported where it sits, and the fix is to await it. To drop the promise on purpose, write `void fn()`. Everywhere else a discarded promise is left alone: a callback slot that starts work and returns is a real pattern, and `Listener.on` is one. Writing `async` yourself ends the propagation, so the caller is now yours too: ```ts Wrong let err = onSubmit(handle, password); // a promise, not a value if (err) { // always true ... } ``` Nothing converts that call, because `onSubmit` has no `@redirect` to an async function. Await it, or keep the callback out of it and put the `try` where the compiler's own await already lands: ```ts Right function onSubmit(handle: string, password: string) { try { signin(handle, password); return null; } catch (e) { return (e as Error).message; } } ``` Plain TypeScript has the same hole. `Promise` is assignable to `void`, which is what the `no-misused-promises` lint rule exists for. ### A method that implements an interface or overrides a base class ```ts Wrong interface Repo { find(id: string): User } class PgRepo implements Repo { find(id: string) { return sql(` select * from users where id = $1 `, [id]).firstOrThrow(); } } function greet(repo: Repo, id: string) { return `hello ${repo.find(id).name}`; // resolves to Repo.find, which has no body } ``` `PgRepo.find` became async. A call through the `Repo` type resolves to the interface signature, so there is nothing for the compiler to await, and `greet` would read `.name` off a promise. The compiler reports the method. Declare the interface member as returning a promise and await calls made through it: ```ts Right interface Repo { find(id: string): Promise } async function greet(repo: Repo, id: string) { return `hello ${(await repo.find(id)).name}`; } ``` The same applies to a base class method overridden by a converted one. If the base method is itself converted, or written `async`, the two agree and nothing is reported. ### A getter, setter, constructor, or parameter default None of these can be `async` in JavaScript, so a converted call inside one is reported: ```ts Reported class Prices { get latest() { return loadPrices(); } // reported constructor() { this.p = loadPrices(); } // reported } function render(prices = loadPrices()) {} // reported ``` Move the call into a method, or assign the parameter at the top of the body when it is undefined. ### A top-level value ```ts Reported export let prices = loadPrices(); // loadPrices queries console.log(loadPrices()); // same ``` A module emits as CommonJS, and CommonJS has no top-level `await`. A top-level call is wrapped in an async function that runs immediately, which works for a statement but not for a value: `prices` would hold a promise, and reading `prices` later is not a call, so nothing waits for it. Move the call inside a function: ```ts Right export function prices() { return loadPrices(); } ``` A top-level call whose result you do not use is fine: ```ts Allowed warmCache(); // runs, nothing waits for it ``` ### A function you marked async yourself Three rules cover a function you wrote `async`: 1. The function is left as you wrote it, and so are its callers. Propagation stops there. A call to it is not awaited for you, and the function making the call does not become `async`. 2. A redirected call inside it, such as `sql()`, is still awaited for you. So is a call to a function that became `async`, and a call to an RPC function in the browser. 3. A direct call to an `*Async` name with no `await` in front of it is left alone, in any function. You get the promise. ```ts Marked async async function save() { // awaited for you (rule 2) sql(` insert into logs ( msg ) values ( 'x' ) `); } function caller() { // not awaited (rule 1) save(); } function start() { // a promise (rule 3) let p = sqlAsync(`select 1`); } ``` `caller` stays a plain function, and `save()` returns a promise there. Writing `async` says you are managing that promise, so awaiting it is yours to do: ```ts Caller async function caller() { await save(); } ``` The same holds for an `async` arrow function, an `async` method, and an RPC function you marked `async`. ## Calls You Await Explicitly Three call shapes pass a function on as a value, so the result is awaited the same way as in any TypeScript program: with `await` or `for await` at the call. - A function invoked reflectively: `await f.call(this)`, `await f.apply(...)`, `await handlers[i]()`. - A callback handed to a library function that uses its return value, such as `xs.sort(...)` or `s.replace(re, ...)`. Compute the values first, then pass a plain callback. - A generator that reads the database becomes `async function*`, and its consumers iterate it with `for await`. ## See Also - `man database` for `sql`, `tx`, and transactions - `man rpc` for calling server code from the browser - `man tests` for `test`, `assert`, and `equal` - `man channel` for `listen` and `notify`