# Data-fetching components handle loading, success, error, and empty states

- **Pattern:** `ab-001289` (`error-resilience.graceful-degradation-shutdown.data-fetching-states`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001289
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001289)

## Why it matters

Components that only handle the success path leave users stranded in three common failure modes: slow network (indefinite spinner), API error (blank or partially rendered page), and empty server response (no indication of why nothing appeared). CWE-703 (improper check for exceptional conditions) applies to missing error state handling. In vibe-coded applications this is one of the most frequent defects: AI code generation reliably handles the success path and omits the loading, error, and empty branches — leaving large surface areas completely unhandled.

## Severity rationale

Low individually, but high in aggregate — missing error and empty states across many data-fetching components produce a product that feels broken on any slow or unreliable connection.

## Remediation

Handle all four states explicitly in every data-fetching component. With TanStack Query or SWR, the state variables are provided; with raw `useEffect`, model them explicitly.

```tsx
// components/item-list.tsx
export function ItemList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['items'],
    queryFn: fetchItems,
  })

  if (isLoading) return <Skeleton />
  if (error) return <ErrorMessage message="Failed to load items. Please refresh." />
  if (!data?.length) return <EmptyState message="No items yet." />
  return <ul>{data.map((item) => <li key={item.id}>{item.name}</li>)}</ul>
}
```

Do not conflate the error state with the empty state — an empty array is not an error, and an error is not an empty array.

## Detection

- **ID:** `data-fetching-states`
- **Severity:** `low`
- **What to look for:** Count all data fetching hooks and components (useQuery, useSWR, fetch in useEffect). Enumerate which handle all 3 states (loading, error, success) vs. which only handle success. Examine components that fetch data (useEffect with fetch, async/await, React Query, SWR, etc.). Check whether they explicitly handle four states: loading (show spinner), success (show data), error (show error message), and empty (show empty state).
- **Pass criteria:** Data-fetching components explicitly handle all four states: loading, success, error, and empty. At least 90% of data fetching components must explicitly handle loading, error, and empty states.
- **Fail criteria:** Components lack explicit error state handling, or don't show empty state, or show loading indefinitely.
- **Skip (N/A) when:** The application has no data-fetching components.
- **Do NOT pass when:** Components handle error state by showing a loading spinner indefinitely instead of an actual error message.

- **Cross-reference:** For graceful API failure, see `graceful-api-failure`.
- **Detail on fail:** `"Data-fetching components lack error state handling. Failed requests show blank screen"` or `"Loading state shown but no empty-state when API returns empty array"`
- **Remediation:** Handle all four states:

  ```tsx
  // components/user-list.tsx — all 3 data states
  if (isLoading) return <Skeleton />
  if (error) return <ErrorMessage message="Failed to load users" />
  if (!data.length) return <EmptyState message="No users found" />
  return <UserTable data={data} />
  ```

  ```tsx
  export function ItemsList() {
    const [items, setItems] = useState([])
    const [loading, setLoading] = useState(true)
    const [error, setError] = useState(null)

    useEffect(() => {
      fetch('/api/items')
        .then(r => r.json())
        .then(data => {
          setItems(data)
          setError(null)
        })
        .catch(err => {
          setError(err)
          setItems([])
        })
        .finally(() => setLoading(false))
    }, [])

    if (loading) return <div>Loading...</div>
    if (error) return <div>Error: {error.message}</div>
    if (items.length === 0) return <div>No items found</div>

    return (
      <ul>
        {items.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
    )
  }
  ```

## External references

- cwe CWE-703
- iso-25010 reliability.fault-tolerance

Taxons: error-resilience

HTML version: https://auditbuffet.com/patterns/ab-001289
