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.
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.
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.
// 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.
ID: error-resilience.graceful-degradation-shutdown.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:
// 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} />
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>
)
}