# Loading, Empty, Error, and Pending States

> Loading, Empty, Error, and Pending States: a worked guide from route registry through to a production build.

Source: [https://askrjs.com/docs/guides/loading-empty-error-and-pending-states](https://askrjs.com/docs/guides/loading-empty-error-and-pending-states)

Status: stable. Packages: @askrjs/askr.

**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.

## Example

Render pending, empty, and recoverable failure states at the query boundary while preserving the last useful context during refreshes.

```tsx
import { Show } from '@askrjs/askr';

<Show when={!projects.pending} fallback={<ProjectSkeleton />}>
  <Show when={!projects.error} fallback={<ProjectError onRetry={projects.refetch} />}>
    <Show when={projects.data.length > 0} fallback={<EmptyProjects />}>
      <ProjectList projects={projects.data} />
    </Show>
  </Show>
</Show>
```

## Goal and architecture

Askr doesn't ship a single 'AsyncBoundary' component — loading, empty, and error rendering are ordinary conditionals over whatever async primitive produced the data. If that's a route loader, use `defer()` and `<Resolve>` so a slow promise doesn't block navigation; if it's a query, its `consistency` field ('fresh' | 'stale' | 'refreshing' | 'pending-write') and stale reason tell you exactly what's happening. Treat 'no data yet' (loading), 'data came back empty' (empty), and 'the request failed' (error) as three distinct branches in your JSX rather than one catch-all spinner-or-content toggle.

## Implementation

For deferred route data, wrap the value in `<Resolve>` and use its pending/rejected render paths — `isDeferred(value)` lets shared components detect a still-pending `Deferred<T>` versus a resolved value. For query-backed views, read `refreshing` and `consistency` off the query object to show a subtle in-place indicator during a background refresh instead of tearing down the whole view; reserve the `@askrjs/ui` `Progress`/`ProgressCircle` components for genuine first-load states. Render the empty state as a real branch (`data.length === 0`) rather than letting an empty array fall through to whatever your list component does with nothing.

## Failure states

A query's stale reason distinguishes `'aborted'`, `'error'`, and `'inconsistent'` — only the `'error'` case should render an error UI; the others usually mean 'still show the last good data, just mark it stale.' Pair this with `@askrjs/fetch`'s result `kind` on the request that populated the query so an HTTP 404 renders 'not found' and a timeout renders 'retry', instead of one generic failure card. Surface transient failures (a background refresh that failed but you still have old data) as a dismissible `Toast`, not a full-page error, since the user hasn't actually lost anything yet.

## Verification

`@askrjs/askr/testing` ships `mockQuery`/`queryState` helpers with `.loading()`, `.error(error, previousData)`, `.refreshing(data)`, `.stale(data, reason)`, and `.pendingWrite(data)` constructors — the last of which is the one that actually exercises the `pending-write` consistency state mentioned above — so you can render each branch of a component against a synthetic query without touching the network. Write one test per branch — loading, empty, error, refreshing-with-stale-data, pending-write — rather than one test that only checks the fresh-data case. For deferred loaders, resolve and reject the underlying promise in tests and assert `<Resolve>` renders the right branch for each outcome.

## Documentation navigation

[Previous](https://askrjs.com/docs/guides/api-integration-and-error-handling/index.md) | [Next](https://askrjs.com/docs/guides/realtime-streams/index.md)
