#vue #javascript #frontend

Day 13: Handling Async State

Welcome to Day 13! Most apps need to fetch data from a server. Today we look at how to handle the “intermediate” states: Loading, Error, and Success.

The Standard Pattern

Without any special features, we usually do this:

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const data = ref(null)
const error = ref(null)
const loading = ref(true)

onMounted(async () => {
  try {
    const res = await fetch('https://api.example.com/data')
    data.value = await res.json()
  } catch (e) {
    error.value = e
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error }}</div>
  <div v-else>
    {{ data }}
  </div>
</template>

This works, but it’s repetitive.

Async Setup & Suspense

Vue 3 supports await directly at the top level of <script setup>.

UserProfile.vue

<script setup lang="ts">
const res = await fetch('https://api.example.com/user')
const user = await res.json()
</script>

<template>
  <h1>{{ user.name }}</h1>
</template>

If you try to use this component, it won’t render… unless you wrap it in a <Suspense> component in the parent.

App.vue

<template>
  <Suspense>
    <!-- Main content -->
    <UserProfile />

    <!-- Loading state -->
    <template #fallback>
      <div>Loading Profile...</div>
    </template>
  </Suspense>
</template>

Note: <Suspense> is still technically an experimental feature, but it’s widely used in the ecosystem (Nuxt 3 uses it extensively).

Handling Errors with onErrorCaptured

If an async component fails, <Suspense> doesn’t handle the error. You need a parent component to “catch” it.

<script setup lang="ts">
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err
  return false // prevent error from propagating further
})
</script>

<template>
  <div v-if="error">Everything is broken: {{ error }}</div>
  <Suspense v-else>
    <UserProfile />
    <template #fallback>Loading...</template>
  </Suspense>
</template>

Challenge for Day 13

  1. Create a component AsyncQuote.
  2. In script setup, await fetch('https://dummyjson.com/quotes/random').
  3. Display the quote.
  4. In App.vue, wrap it in Suspense. Show “Fetching wisdom…” while loading.

Solution:

AsyncQuote.vue

<script setup lang="ts">
const res = await fetch('https://dummyjson.com/quotes/random')
const data = await res.json()
</script>

<template>
  <blockquote>"{{ data.quote }}" - {{ data.author }}</blockquote>
</template>

App.vue

<template>
  <h1>Daily Wisdom</h1>
  <Suspense>
    <AsyncQuote />
    <template #fallback>
      <p>Fetching wisdom...</p>
    </template>
  </Suspense>
</template>

Tomorrow is the big day. We combine Router, Pinia, and Async into a final project!