---
title: "Async Data and Fetching With resource()"
description: "Async data with resource(), a tracked fetcher the engine owns: previous requests abort, only the latest response writes, and unmount aborts everything."
source: https://lemonadejs.com/docs/async/
---

# Async data with resource()

Async data has one tool: `resource()`, for data that arrives later. Give it a
fetcher;
get back three plain states and a handle:

```javascript
const User = component('user', { id: 0 }, (props, { resource }) => {
    const user = resource((signal) =>
        fetch('/api/users/' + props.id.value, { signal }).then((r) => r.json())
    );
    return html`<div>
        ${() => user.loading.value && html`<i>loading…</i>`}
        ${() => user.error.value && html`<i>failed</i>`}
        ${() => user.data.value && html`<b>${user.data.value.name}</b>`}
    </div>`;
});
```

The fetcher is **tracked** (`props.id` changing re-runs it), and the
engine owns the lifecycle: the previous request is aborted, only the
latest response ever writes, unmount aborts everything.

`resource()` re-runs when the states it reads change, aborts the stale request and exposes `loading`, `error` and `data`.

<!--example-->

```js
import { html } from 'lemonadejs';

// a fake API: answers after 700 ms, and id 4 does not exist
const fetchUser = (id, signal) => new Promise((resolve, reject) => {
    const names = { 1: 'Ana', 2: 'Bruno', 3: 'Carla' };
    const t = setTimeout(() => (names[id] ? resolve({ id, name: names[id] }) : reject(new Error('404 not found'))), 700);
    signal.addEventListener('abort', () => clearTimeout(t));
});

const App = (props, { state, resource }) => {
    const id = state(1);
    const user = resource((signal) => fetchUser(id.value, signal));   // tracked: re-runs when id changes

    return html`<div>
        ${[1, 2, 3, 4].map((n) => html`<button onclick="${() => (id.value = n)}">user ${n}</button>`)}
        <button onclick="${() => user.reload()}">reload</button>
        <p>
            ${() => user.loading.value && html`<i>loading…</i>`}
            ${() => user.error.value && html`<span style="color:#c00">${user.error.value.message}</span>`}
            ${() => user.data.value && !user.loading.value && html`<b>${user.data.value.name}</b> (id ${user.data.value.id})`}
        </p>
    </div>`;
};
```

## The problem

Fetching inside a component is three lifecycle problems wearing one
`await`:

- **The out-of-order race.** Request A goes out, the input changes,
  request B goes out, A resolves *after* B, and the stale response
  overwrites the fresh one. Nothing throws; the screen is simply wrong.
  This failure needs a slow network to reproduce, which is why it ships.
- **The zombie write.** The component unmounts while a request is in
  flight; the response lands and writes into a dead instance. The standard
  guards (an `ignore` flag set in a cleanup function, or an
  `AbortController` aborted there) are correct, but written by hand, per
  fetch site.
- **The bookkeeping triplet.** `data`, `loading`, `error` plus the
  transitions between them (does a new attempt clear the old error? does
  a late rejection from an *aborted* request set `error`?): a state
  machine every component reinvents.

Hand-rolled guards have a specific property: they are *absent by
default*. The natural code, `fetch().then(set)`, is the racy code,
and the guard is a discipline the author must remember each time.

## How LemonadeJS solves it

`resource()` moves the guards from convention into the engine, so the
natural code is the guarded code:

- **Tracked re-fetch.** The fetcher runs through dependency tracking
  like a template expression: any state it reads (props, filters, ids)
  re-runs it on change.
- **Abort-previous.** Each re-run aborts the previous request's
  `AbortSignal`. Pass it to `fetch` and the network request itself is
  cancelled, not just ignored.
- **Latest-wins.** Each run is numbered; a response from a superseded
  run writes nothing. The out-of-order race is not a bug you avoid; it
  is a write that cannot happen.
- **Unmount aborts.** Disposal aborts the in-flight request and retires
  every pending write. The zombie write is structurally gone.
- **Quiet edges.** A rejection from a superseded or aborted request is
  silent: it never sets `error`. A *synchronous* throw in the fetcher
  lands in `error` instead of escaping as an unhandled exception. A new
  attempt clears `error` and raises `loading` immediately. All of these
  transitions are pinned in the engine suite (`tests/resource.test.ts`).
- **`reload()` + `peek()` for imperative timing.** `reload()` re-runs
  the fetcher without re-tracking. When *you* decide the timing (a
  debounce timer, a refresh button), read inputs with `peek()` inside
  the fetcher so no tracked dependency competes with your schedule.

One loop remains possible, so it has a name: a fetcher that reads the
resource's **own** `data`/`loading`/`error` re-triggers itself when the
response lands. That is an async cycle the synchronous `LJS-203` loop guard
cannot see, because the cycle crosses an `await`. Dev builds warn
`LJS-206`. Derive *from* `resource.data` elsewhere, in slots or
[computed()](/docs/computed/), never inside the fetcher.

**Scope is held deliberately.** `resource()` is fetch lifecycle, full
stop: no cache, no dedup, no retry, no invalidation. Those are
application policies with application-specific answers, and they compose
on top: a module-scope [store()](/docs/store/) as a cache, your retry
policy in the fetcher.

## In practice

**The timeline deleted its `alive` flag.** The v5 timeline guarded its
remote fetch with a manual liveness flag; the v6 port runs on
`resource()` and the flag is gone, and with it a latent bug: the v5
code checked `alive` but had no answer to *out-of-order* responses, a
race that was simply never written down. The shipped source documents
the trade:

```javascript
// v5 fetchRemote on the resource() tool: ... The fetcher PEEKS everything
// (refresh() decides WHEN via reload()) and the engine owns the
// lifecycle: a new request aborts the stale one, only the latest
// response lands, unmount aborts (v5's alive flag and its out-of-order
// race are gone).
const remote = resource((signal) => { ... });
```

**The dropdown's remote search.** Autocomplete against `url?q=` is the
canonical race: type "ab", then "abc". Two requests are in flight, and the
shorter query often resolves last. The dropdown's debounce timer commits
the query and calls `reload()`; the fetcher peeks. A newer search aborts
the stale request and only the latest response lands, where the old
then-chain implementation had exactly the window the engine now closes.

```javascript
// the imperative-timing pattern (debounced remote search)
const results = resource((signal) =>
    fetch(url.peek() + '?q=' + term.peek(), { signal }).then((r) => r.json())
);
const onType = (q) => {
    clearTimeout(timer);
    timer = setTimeout(() => { term.value = q; results.reload(); }, 300);
};
```

**The engine suite is the contract.** Out-of-order responses resolved in
reverse order leave the latest data in place; unmount mid-flight aborts
the signal and a late response writes nothing, not even `loading` moves
again; an aborted request's rejection never reaches `error`. Each is a
test, not a promise.

## Reference

```javascript
const r = resource(fetcher);          // component tool
// fetcher: (signal: AbortSignal) => T | Promise<T>
//   TRACKED: state reads re-run it (use peek() to read without tracking)
//   pass signal to fetch() so aborts cancel the network request

r.data       // State<T | undefined>     last successful value
r.loading    // State<boolean>           true while a run is in flight
r.error      // State<unknown|undefined> last failure; cleared on new attempt
r.reload()   // re-run imperatively (no re-tracking)
```

- Previous run aborts on re-run; only the latest run's response writes.
- Unmount aborts in-flight work; nothing writes after disposal.
- Superseded/aborted rejections are silent; synchronous throws → `error`.
- Never read `r.data`/`r.loading`/`r.error` *inside* the fetcher:
  `LJS-206`, the async update loop.
- Scope: fetch lifecycle only. Cache, dedup, retry are app policies;
  compose with [store()](/docs/store/).