• Fri, Mar 2026

Vue 3 Suspense and Async Components Made Simple

Vue 3 Suspense and Async Components Made Simple

If you’ve ever struggled with showing loading states while waiting for API calls or lazily loading components in Vue, you’ll love Vue 3 Suspense. Suspense provides a clean way to handle asynchronous operations in your components, making your UI smoother and your code easier to manage.

What is Vue 3 Suspense?

Suspense is a built-in component in Vue 3 that lets you handle async dependencies in components. When a component is loading data or an async resource, Suspense allows you to display fallback content such as a loading spinner until the async task resolves.

Why Use Suspense?

  • Manage loading states without cluttering your components.
  • Gracefully handle errors when fetching async data.
  • Lazy load heavy components only when needed.
  • Improve user experience with clean fallback UIs.

Basic Structure of Suspense


<Suspense>
  <template #default>
    <AsyncComponent />
  </template>

  <template #fallback>
    <p>Loading...</p>
  </template>
</Suspense>

Here’s what’s happening:

  • #default: The component you want to render asynchronously.
  • #fallback: What to show while the default component is still resolving.

Step 1: Creating an Async Component

Vue provides a helper function defineAsyncComponent to load components lazily.


// AsyncComponent.js
import { defineAsyncComponent } from 'vue'

export default defineAsyncComponent(() => 
  import('./UserProfile.vue')
)

This will load UserProfile.vue only when required, which is especially useful for large applications.

Step 2: Using Suspense with Async Setup

Another exciting feature in Vue 3 is async setup. You can return a Promise from setup, and Suspense will automatically wait for it.


<!-- UserProfile.vue -->
<template>
  <div>
    <h2>User Profile</h2>
    <p>Name: {{ user.name }}</p>
    <p>Email: {{ user.email }}</p>
  </div>
</template>

<script setup>
async function fetchUser() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users/1')
  return res.json()
}

const user = await fetchUser()
</script>

Since setup is async here, Vue Suspense will automatically handle the waiting state for us.

Step 3: Wrapping in Suspense

Now we combine everything inside the parent app:


<!-- App.vue -->
<template>
  <div>
    <h1>Vue 3 Suspense Example</h1>
    <Suspense>
      <template #default>
        <UserProfile />
      </template>

      <template #fallback>
        <p>Loading user data...</p>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import UserProfile from './UserProfile.vue'
</script>

Step 4: Handling Errors in Suspense

We can extend defineAsyncComponent with options for error handling and retries.


// AsyncWithErrorHandling.js
import { defineAsyncComponent } from 'vue'

export default defineAsyncComponent({
  loader: () => import('./UserProfile.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: {
    template: '<p>Failed to load component</p>'
  },
  loadingComponent: {
    template: '<p>Loading...</p>'
  }
})

Real-World Example: Async Data + Notifications

Let’s put this into practice with a realistic use case: fetching user data with loading and error handling, while showing a notification on error.


<!-- App.vue -->
<template>
  <div>
    <h1>Vue 3 Suspense Real-World Example</h1>

    <Suspense>
      <template #default>
        <UserProfile />
      </template>

      <template #fallback>
        <div>Fetching user profile...</div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import UserProfile from './UserProfile.vue'
</script>

<!-- UserProfile.vue -->
<template>
  <div v-if="user">
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>

  <div v-else>
    <p>No user data found</p>
  </div>
</template>

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

async function fetchUser() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users/1')
  if (!res.ok) throw new Error('Failed to fetch user')
  return res.json()
}

const user = ref(null)
try {
  user.value = await fetchUser()
} catch (err) {
  console.error('Error:', err.message)
}
</script>

Now, when the UserProfile component is loading, the fallback content appears. If there’s an error, it logs it and gracefully shows “No user data found.”

Best Practices for Suspense

  • Use Suspense for components that rely on async setup or API calls.
  • Always provide a meaningful fallback (spinner, skeleton loader, or message).
  • Combine with defineAsyncComponent for lazy loading routes or heavy UI pieces.
  • Keep error handling user-friendly.

Final Thoughts

Vue 3 Suspense is one of those features that makes your life easier as a developer while improving UX for your users. Whether you’re fetching data from APIs or lazy-loading components, Suspense ensures a smooth loading and error-handling experience. With defineAsyncComponent and async setup, you now have powerful tools to handle async workflows in a clean and declarative way.

This website uses cookies to enhance your browsing experience. By continuing to use this site, you consent to the use of cookies. Please review our Privacy Policy for more information on how we handle your data. Cookie Policy