Async Values
Handle async data with loading, error, and success states
Async Values
Koze lets you write async data-dependent templates without
blocking SSR. Call an async function without await and the binding
becomes an AsyncValue<T> — an object exposing pending, error,
and success state alongside the eventual value. The initial page
responds immediately with the pending branch of your template; when
the promise settles, the resolved branch streams in and the browser
swaps it into place.
Two patterns
| Pattern | Returns | Use case |
|---|---|---|
const x = fn() |
AsyncValue<T> |
Need loading / error states; SSR stays non-blocking. |
const x = await fn() |
T |
Just need the value — SSR blocks until it resolves. |
AsyncValue API
When you call an async function without await, the binding exposes
three observable flags:
interface AsyncValue<T> extends T {
pending: boolean; // true while loading
error: string | null; // error message if rejected, null otherwise
success: boolean; // true when resolved
}
The AsyncValue is the value — properties and iteration work
directly. No .data wrapper.
Basic usage
<script>
import { getTodos } from '$server/todos';
const todos = getTodos(); // no await → AsyncValue<Todo[]>
</script>
if (todos.pending) {
<div class="skeleton">Loading…</div>
} else if (todos.error) {
<p class="error">Failed: {todos.error}</p>
} else if (todos.success) {
for (const todo of todos) {
<TodoItem todo={todo} />
}
}
That's the whole thing. No <Boundary> tag, no lifecycle hook, no
custom component — plain template control flow. The framework spots
the .pending access on todos, compiles the if/else-if/else chain
into a streaming boundary automatically, and sends the initial HTML
(with the pending branch rendered) while the real call runs in the
background.
Note: Koze does not use Svelte's
{#await}block syntax. Async state is modeled as normal JavaScript data: call withoutawait, branch on.pending,.success, or.error, and let the compiler turn that if-chain into the boundary.
Note:
.pendingis the signal. The framework detects async bindings by looking forX.pendingin the template. Any top-level<script>binding referenced that way becomes an AsyncValue; the surrounding if-chain becomes a stream boundary. Server-action state also exposes.pending, but statefulaugment()aliases are action state, not streamed AsyncValues. See Actions for form state.
Region inference
The framework streams the smallest contiguous if/else-if/else chain whose head condition references an async binding. Everything else on the page renders synchronously on the first response.
<script>
const issue = await getIssue(id); // awaited — blocks SSR
const ai = analyze(issue.key); // non-awaited — streamed
</script>
<h1>{issue.title}</h1> <!-- rendered on first response -->
<p>{issue.description}</p> <!-- rendered on first response -->
if (ai.pending) { <!-- this chain is the boundary -->
<Skeleton />
} else if (ai.error) {
<p>{ai.error}</p>
} else if (ai.success) {
<p>{ai.summary}</p>
}
<footer>©{year}</footer> <!-- rendered on first response -->
Only the if-chain is a boundary — the heading, the description, and
the footer are in the initial HTML. When analyze(...) resolves, the
framework streams the chain's resolved markup and swaps it in place of
the <Skeleton />.
Multiple async values in one chain
When a chain's condition references more than one async binding, the
framework combines their promises with Promise.all and re-renders
the chain once all bindings have settled:
<script>
const user = getUser();
const notifications = getNotifications();
</script>
if (user.pending || notifications.pending) {
<DashboardSkeleton />
} else if (user.error || notifications.error) {
<p>Failed to load: {user.error || notifications.error}</p>
} else if (user.success && notifications.success) {
<Dashboard user={user} notifications={notifications} />
}
Both user and notifications receive the same AsyncValue state
(pending → success or error). If either promise rejects, the chain
takes the error branch; the error message is whichever rejection
won the race.
Combining into one Promise
If you want several async sources to land atomically as a single
value, combine them with Promise.all at the binding site:
<script>
import { suggestCategory, listPaths } from '$server/taxonomy';
const panel = Promise.all([
suggestCategory(key),
listPaths(),
]).then(([suggestion, paths]) => ({ suggestion, paths }));
</script>
if (panel.pending) {
<Skeleton />
} else if (panel.error) {
<p>{panel.error}</p>
} else if (panel.success) {
<h3>{panel.suggestion.bestPath}</h3>
<small>{panel.paths.length} paths considered</small>
}
This shape gives you a single AsyncValue whose success branch can destructure the combined result.
Blocking (await)
When a page doesn't make sense without the data — title, authentication,
the matching record — use await:
<script>
const todos = await getTodos(); // blocks SSR until resolved
</script>
for (const todo of todos) {
<TodoItem todo={todo} />
}
A good rule of thumb: await what the user would leave the page over
if it were missing. Non-awaited AsyncValues are for the rest.
Browser RPC and invalidation
When a $server/* function is called from the browser, the generated
stub returns the same thenable AsyncValue<T> shape:
<script>
import { refreshTodos } from '$server/todos';
async function refresh() {
const result = refreshTodos();
if (result.pending) {
console.log('Refreshing...');
}
await result;
}
</script>
<button>Refresh</button>
The browser call goes through Koze's Capn Web channel endpoint. On
success or error, the runtime dispatches koze:invalidate-reads so
hydrated reactive effects can re-run against the latest server state.
Success guard
Use else if (x.success) in place of a naked else when the template
body needs data from the resolved value. The pending AsyncValue is an
empty object — accessing .data or iterating it during pending
renders nothing, which is rarely what you want.
if (todos.pending) {
<Skeleton />
} else if (todos.error) {
<p>Failed: {todos.error}</p>
} else if (todos.success) {
for (const todo of todos) {
<TodoItem todo={todo} />
}
}
Type safety
AsyncValue<T> extends T, so type inference flows through:
interface Todo {
id: string;
title: string;
done: boolean;
}
// getTodos returns Promise<Todo[]>; bound without await: AsyncValue<Todo[]>
const todos = getTodos();
if (todos.success) {
for (const todo of todos) {
console.log(todo.title); // string
console.log(todo.done); // boolean
}
}
Live workflow status
For Cloudflare Workflow instance status — which changes over time —
use workflowStatus
from koze:workflow. It returns the same AsyncValue shape and,
when { poll } is passed, the framework re-renders the whole route on
each tick.
<script>
import { params } from 'koze:request';
import { workflowStatus } from 'koze:workflow';
const status = await workflowStatus('migration', params.id, { poll: '2s' });
</script>
Runtime helpers
Import from koze for AsyncValue construction and type
guarding outside of streamed boundaries:
import {
createPendingValue,
createSuccessValue,
createErrorValue,
isAsyncValue,
type AsyncValue,
} from '@kuratchi/koze';
| Helper | Purpose |
|---|---|
createPendingValue<T>() |
Returns an empty AsyncValue with pending=true. |
createSuccessValue<T>(v) |
Wraps a resolved value with success=true. |
createErrorValue<T>(msg) |
Returns an AsyncValue with error=msg. |
isAsyncValue(v) |
Type guard for inspecting unknown values. |