# 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. The tag ships in the package's type declarations, so you can read it yourself: ```bash grep -A2 "@redirect" node_modules/@elements/app/server/index.d.ts ``` That means you never have to guess whether a function makes yours async. Look at its declaration. If it has `@redirect`, it does. ## Which Functions Participate Database: ``` sql tx begin commit rollback connect end listen notify checkout ``` Sessions, email, jobs, files: ``` session.login session.logout session.renew session.revoke email send schedule file.read ``` LiveTable writes, from the browser: ``` insert update delete ``` 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: see `man tests`. RPC functions are converted too. A function tagged `@rpc` 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. ## Which Name To Write Two rules decide it, and neither needs judgement. **Write the plain name.** The compiler inserts the `await`. Never write `await` on a plain-name call 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 plain name. 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); // plain: 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`, whose promise is owned by the test runner and is never yours to compose. 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 plain-name 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 There are three cases the compiler does not handle. Each one reports an error, so you will not hit them silently. ### A callback parameter 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 will not fix this by rewriting `expectError`, because changing a function based on what one of its callers passed would make a file's output depend on the files that import it. Elements compiles the other way around. So it reports the error and you widen the parameter: ```ts function expectError(desc: string, fn: () => void | Promise) { ``` Adding `Promise` to the return type allows an async callback. It does not make the compiler await it for you. Handle the returned value where you call it: ```ts let result = fn(); if (result != null && typeof result.then === "function") { return result.then( () => errorf("expected %v to fail", desc), (e) => assert(e instanceof ValidationError), ); } ``` Note that plain TypeScript has the same hole. `Promise` is assignable to `void`, which is what the `no-misused-promises` lint rule exists for. ### A top-level variable ```ts export let prices = loadPrices(); // loadPrices queries ``` 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 to declarations that carry `@redirect`. Once you write `async` yourself, awaiting it is yours to do: ```ts async function caller() { await save(); } ``` ## 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`