100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
JavaScript

Handling Loading and Error States

Explore patterns for modeling async status explicitly in Vue components so templates can reliably render loading, error, empty, and success UI.

Working with Data & APIsIntermediate8 min readJul 9, 2026
Analogies

Handling Loading and Error States

Any component that fetches data asynchronously has at minimum four states to represent: idle/not-yet-started, loading, error, and success (which itself can further split into 'success but empty' and 'success with data'). A surprisingly common bug source is representing these states implicitly through combinations of nullable refs — for example inferring 'loading' from data.value === null && error.value === null — which becomes ambiguous and fragile as soon as a component needs to distinguish 'never fetched' from 'fetched and got null back'. Modeling status explicitly, rather than inferring it, keeps templates predictable and easy to reason about.

🏏

Cricket analogy: Before a match, the scoreboard shows "yet to bat", "batting", "all out", and "innings complete with score" as distinct states — a commentator conflating "yet to bat" with "all out" because both show zero runs would mislead viewers, just as inferring loading from null refs misleads the UI.

Three Booleans vs. a Single Status Ref

A common beginner pattern uses three independent refs — isLoading, isError, data — but nothing stops all three from being simultaneously inconsistent (e.g. isLoading and isError both true) unless the code is disciplined about resetting them. A more robust alternative is a single status ref constrained to a small set of string literals: 'idle' | 'loading' | 'success' | 'error'. This makes invalid states unrepresentable — the template can switch on one value instead of checking several booleans in combination — and pairs naturally with TypeScript's literal union types for compile-time safety.

🏏

Cricket analogy: Instead of separately tracking "isBatting", "isBowling", "isFielding" flags that could all be true at once, a scorer uses one state field per player — "batting", "bowling", or "fielding" — like Virat Kohli's status card showing exactly one value, never a contradictory combination.

vue
<script setup>
import { ref, computed } from 'vue'

const status = ref('idle') // 'idle' | 'loading' | 'success' | 'error'
const data = ref(null)
const error = ref(null)

async function loadReport(id) {
  status.value = 'loading'
  try {
    const res = await fetch(`/api/reports/${id}`)
    if (!res.ok) throw new Error('Failed to load report')
    data.value = await res.json()
    status.value = 'success'
  } catch (err) {
    error.value = err
    status.value = 'error'
  }
}

const isEmpty = computed(
  () => status.value === 'success' && Array.isArray(data.value) && data.value.length === 0
)
</script>

<template>
  <p v-if="status === 'loading'">Loading report</p>
  <p v-else-if="status === 'error'">{{ error.message }}</p>
  <p v-else-if="isEmpty">No report data found.</p>
  <ReportView v-else-if="status === 'success'" :data="data" />
</template>

Distinguishing Empty From Loading

Templates should never render a 'success' view with an empty list as if it were still loading, and they should never show a spinner forever if a request legitimately returns zero results. Treat 'empty but successful' as its own renderable branch — usually a friendly message rather than a blank area — since to a user an unexplained blank screen looks indistinguishable from a bug.

🏏

Cricket analogy: A "no results found" screen after a player-stats search should show "No centuries recorded for this player" rather than a blank scoreboard — a blank scoreboard looks exactly like the match hasn't started, confusing the viewer.

React developers building custom hooks around fetch usually converge on this same idle/loading/success/error shape (sometimes literally called a 'status' or 'fetchState'), and libraries like TanStack Query expose exactly these fields out of the box — Vue's ecosystem equivalents mirror the same design because the underlying problem is framework-agnostic.

Forgetting to reset error.value = null at the start of a new fetch attempt is a frequent bug: a user retries after a failed request, the new request succeeds, but a stale error message from the previous attempt still renders because nothing cleared it before the retry began.

Surfacing Errors Usefully

Catching an error and setting error.value = err is only half the job — the template needs to translate that error into something a user can act on. Distinguish network failures (offer a retry button) from validation errors (show the specific field problem) from 404s (show a 'not found' state) rather than collapsing everything into one generic 'Something went wrong' message, which frustrates users who can't tell if retrying will help.

🏏

Cricket analogy: A rain-delay notice tells fans "match suspended, resuming at 3pm" while a rescheduled-venue notice says "shifted to a different ground" — collapsing both into "match status unclear" leaves fans unable to act, unlike distinguishing network failure from a 404.

  • Model async state explicitly (loading/error/success/idle) rather than inferring it from combinations of nullable refs.
  • A single status ref with a small string-literal union avoids impossible/inconsistent state combinations.
  • Treat 'successful but empty' as its own template branch, distinct from both loading and error.
  • Always reset stale error state at the start of a new fetch attempt, especially on retry.
  • Differentiate error types (network, validation, not-found) in the UI rather than showing one generic message.
  • This four-state pattern (idle/loading/success/error) is framework-agnostic and mirrored by data-fetching libraries.

Practice what you learned

Was this page helpful?

Topics covered

#JavaScript#VueJsStudyNotes#WebDevelopment#HandlingLoadingAndErrorStates#Handling#Loading#Error#States#StudyNotes#SkillVeris