# Async Most functions in an Elements app need no `async` and no `await`. You write ordinary synchronous code. The compiler adds `async` and `await` for you, and it does so up the whole call stack. This is driven by build tags, not by guesswork. A function opts in with a `@redirect` tag in its JSDoc. When you call a function that carries that tag, the compiler rewrites the call to await the async version, marks your function `async`, and then does the same to everything that calls your function. ## 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 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` carries a `@redirect` tag. `greet` became async because it calls `findUser`. ## How the Build Tag Works Here is the redirect tag on `sql`, in `@elements/app`: ```ts /** * @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`, emit `await sqlAsync` instead. The synchronous `sql` is the signature you write against. `sqlAsync` is what actually runs. ## How To Tell Which Functions Participate The tag is the signal. A function participates if, and only if, its declaration carries `@redirect`. The tag ships in the package's type declarations, so you never have to guess: ```bash 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 `@redirect`, the compiler awaits the call. If it does not, the function is ordinary TypeScript and the usual rules apply. These are the redirected functions in `@elements/app` today. Each has a counterpart with the same name and an `Async` suffix, except where noted. Database, from `@elements/app`: ``` sql tx checkoutDb ``` Methods on a `Database` or `DatabasePool`: ``` connect end checkout begin commit rollback sql tx listen notify ``` Channels, sessions, email, jobs, files: ``` 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: ``` view insert update delete more ``` Tests: ```ts 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 */ 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 without a `@redirect` tag 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 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 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 You Have to Do It Yourself 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 will not rewrite it: changing a declaration based on what one caller passed would make a file's output depend on the files that import it, and Elements compiles the other way around. Every one of these cases reports an error, so you will not hit them silently. ### A callback declared without a Promise ```ts 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 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 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 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 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 let err = onSubmit(handle, password); // a promise, not a value if (err) { ... } // always true ``` Nothing converts that call, because `onSubmit` carries no `@redirect`. Await it, or keep the callback out of it and put the `try` where the compiler's own await already lands: ```ts 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 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 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 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 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 export function prices() { return loadPrices(); } ``` A top-level call whose result you do not use is fine: ```ts warmCache(); // runs, nothing waits for it ``` ### A function you marked async yourself ```ts async function save() { await sqlAsync(`insert into logs (msg) values ('x')`); } function caller() { save(); // not awaited } ``` The compiler only converts calls that reach a `@redirect` tag. Once you write `async` and call the `*Async` names yourself, awaiting your function is yours to do: ```ts async function caller() { await save(); } ``` ## Known Gaps Three shapes are neither converted nor reported. Each is rare, and each is the same failure: a promise where a value was expected. - A converted function invoked reflectively: `f.call(this)`, `f.apply(...)`, `handlers[i]()`. - A converted callback passed to a library function that consumes its return value: `xs.sort((a, b) => ...)`, `s.replace(re, () => ...)`. Library callback slots are declared in `.d.ts` files you cannot edit, so they are not reported. - A generator that becomes `async function*`: its consumers need `for await`, which the compiler does not add. ## 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`