# Async Values

> Handle async data with loading, error, and success states

Package: Koze
Canonical: https://kuratchi.dev/docs/koze/async-values
Markdown: https://kuratchi.dev/docs/koze/async-values.md

## 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:

```ts
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

```html
<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 without `await`, branch on
`.pending`, `.success`, or `.error`, and let the compiler turn that
if-chain into the boundary.

> **Note:**
**`.pending` is the signal.** The framework detects async bindings by
looking for `X.pending` in 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 stateful `augment()` aliases are action state, not
streamed AsyncValues. See [Actions](/docs/koze/actions#augmented-forms)
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.

```html
<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:

```html
<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:

```html
<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`:

```html
<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:

```html
<script>
  import { refreshTodos } from '$server/todos';

  async function refresh() {
    const result = refreshTodos();

    if (result.pending) {
      console.log('Refreshing...');
    }

    await result;
  }
</script>

<button onclick={refresh()}>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.

```html
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:

```ts
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`](/docs/koze/workflows#live-status-with-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.

```html
<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:

```ts
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. |
