Skip to content

Async

The async utility wraps an async function into a reactive, awaitable controller. You can start, stop, and rerun async operations, and read their state, value, and errors as reactive signals.

Basic usage

import {
function async<TInput = any, TOutput = undefined>(fn: MaybeReactive<(input: TInput) => Promise<TOutput>>): Async<TInput, TOutput> & ((...args: any[]) => TOutput | undefined)

Create an

Async

that is also callable as a signal: invoking it (with no args) reads the current result, so it drops into any reactive context that expects a zero-arg getter.

@example

import { async } from "elements-kit/utilities/async";
const load = async((id: string) => fetch(`/u/${id}`).then(r => r.json()));
load.run("alice");
// Read as a signal β€” subscribes to result changes
effect(() => console.log(load()));
await load; // Await the current run β€” works like a normal promise

async
} from "elements-kit/utilities/async";
const
const fetchItems: Async<any, any> & ((...args: any[]) => any)
fetchItems
=
async<any, any>(fn: MaybeReactive<(input: any) => Promise<any>>): Async<any, any> & ((...args: any[]) => any)

Create an

Async

that is also callable as a signal: invoking it (with no args) reads the current result, so it drops into any reactive context that expects a zero-arg getter.

@example

import { async } from "elements-kit/utilities/async";
const load = async((id: string) => fetch(`/u/${id}`).then(r => r.json()));
load.run("alice");
// Read as a signal β€” subscribes to result changes
effect(() => console.log(load()));
await load; // Await the current run β€” works like a normal promise

async
(() =>
function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
fetch
("/api/items").
Promise<Response>.then<any, never>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<any>

Attaches callbacks for the resolution and/or rejection of the Promise.

@param ― onfulfilled The callback to execute when the Promise is resolved.

@param ― onrejected The callback to execute when the Promise is rejected.

@returns ― A Promise for the completion of which ever callback is executed.

then
((
res: Response
res
) =>
res: Response
res
.
Body.json(): Promise<any>
json
()));
const fetchItems: Async<any, any> & ((...args: any[]) => any)
fetchItems
.
Async<any, any>.start(...args: [] | [input: any]): Async<any, any> & ((...args: any[]) => any)

Starts a new reactive async operation, stopping any currently active one.

start
(); // begin reactive execution

Control methods

Start for reactive execution, run for one-shot, stop to tear down.

op.start(); // run and track reactive dependencies β€” reruns when signals change
op.run(); // run once without tracking β€” does not rerun on signal changes
op.stop(); // stop reactive reruns and run cleanup logic

Async implements Symbol.dispose, so using stops it automatically when it goes out of scope:

{
using op = async(() => fetch("/api/data").then((r) => r.json())).start();
await op;
console.log(op.value);
} // op.stop() called automatically here

Reactive state

Async exposes the same reactive state interface as ReactivePromise:

op.state; // "pending" | "fulfilled" | "rejected"
op.value; // resolved value (T | undefined)
op.reason; // rejection reason (E | undefined)
op.result; // value if fulfilled, reason if rejected, undefined if pending
op.pending; // true while pending

All properties are reactive β€” reading them inside an effect or computed subscribes to changes.

Callable signal

An Async instance is also callable as a signal. op() returns op.result and tracks it as a reactive dependency:

import { effect } from "elements-kit/signals";
effect(() => {
const result = op(); // undefined while pending, T when fulfilled, E when rejected
console.log(result);
});

This makes Async composable with computed and templates.

Awaitable

Async implements .then, .catch, and .finally, so you can await it directly:

const op = async(() => Promise.resolve(123)).start();
const value = await op; // 123

Reactive reruns

Read signals inside the async function to make it re-execute when they change. Only signal reads before the first await are tracked.

import {
function signal<T>(): Updater<T> & Computed<T> (+1 overload)

Creates a mutable reactive signal.

  • Read: call with no arguments β†’ returns the current value and subscribes the active tracking context.
  • Write: call with a value β†’ updates the signal and schedules downstream effects if the value changed.

@example

const count = signal(0);
count(); // β†’ 0 (read)
count(1); // write – effects depending on count will re-run
count(); // β†’ 1

signal
} from "elements-kit/signals";
import {
function async<TInput = any, TOutput = undefined>(fn: MaybeReactive<(input: TInput) => Promise<TOutput>>): Async<TInput, TOutput> & ((...args: any[]) => TOutput | undefined)

Create an

Async

that is also callable as a signal: invoking it (with no args) reads the current result, so it drops into any reactive context that expects a zero-arg getter.

@example

import { async } from "elements-kit/utilities/async";
const load = async((id: string) => fetch(`/u/${id}`).then(r => r.json()));
load.run("alice");
// Read as a signal β€” subscribes to result changes
effect(() => console.log(load()));
await load; // Await the current run β€” works like a normal promise

async
} from "elements-kit/utilities/async";
const
const id: Updater<number> & Computed<number>
id
=
signal<number>(initialValue: number): Updater<number> & Computed<number> (+1 overload)

Creates a mutable reactive signal.

  • Read: call with no arguments β†’ returns the current value and subscribes the active tracking context.
  • Write: call with a value β†’ updates the signal and schedules downstream effects if the value changed.

@example

const count = signal(0);
count(); // β†’ 0 (read)
count(1); // write – effects depending on count will re-run
count(); // β†’ 1

signal
(1);
const
const fetchTodo: Async<any, any> & ((...args: any[]) => any)
fetchTodo
=
async<any, any>(fn: MaybeReactive<(input: any) => Promise<any>>): Async<any, any> & ((...args: any[]) => any)

Create an

Async

that is also callable as a signal: invoking it (with no args) reads the current result, so it drops into any reactive context that expects a zero-arg getter.

@example

import { async } from "elements-kit/utilities/async";
const load = async((id: string) => fetch(`/u/${id}`).then(r => r.json()));
load.run("alice");
// Read as a signal β€” subscribes to result changes
effect(() => console.log(load()));
await load; // Await the current run β€” works like a normal promise

async
(() =>
function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
fetch
(`https://jsonplaceholder.typicode.com/todos/${
const id: () => number (+1 overload)
id
()}`) // tracked
.
Promise<Response>.then<any, never>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<any>

Attaches callbacks for the resolution and/or rejection of the Promise.

@param ― onfulfilled The callback to execute when the Promise is resolved.

@param ― onrejected The callback to execute when the Promise is rejected.

@returns ― A Promise for the completion of which ever callback is executed.

then
((
res: Response
res
) =>
res: Response
res
.
Body.json(): Promise<any>
json
()),
).
Async<any, any>.start(...args: [] | [input: any]): Async<any, any> & ((...args: any[]) => any)

Starts a new reactive async operation, stopping any currently active one.

start
(); // re-fetches automatically when id changes
const id: (value: number) => void (+1 overload)
id
(2); // triggers a new fetch

run() is untracked β€” signals inside the fn do not trigger re-runs. To get reactive reruns with explicit parameters, wrap it in an external effect:

import { effect, signal } from "elements-kit/signals";
const todoId = signal(1);
effect(() => {
fetchTodo.run(todoId()); // re-fetches when todoId changes (tracked by outer effect)
});

Cleanups

Register cleanup logic inside your async function using onCleanup. It runs when stop() is called or when start() re-runs due to a signal change:

import { onCleanup } from "elements-kit/signals";
const query = async((id: number) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
return fetch(`/api/todos/${id}`, { signal: controller.signal }).then((r) =>
r.json(),
);
}).start();

onCleanup also works inside run() β€” the cleanup fires when stop() is called or when the next run() replaces it.

See also

  • Promise β€” the underlying ComputedPromise / ReactivePromise primitive.
  • Data fetching β€” full recipe composing retry, online, window-focus.
  • Signals β€” onCleanup, untracked.