Vue <Suspense> is a built-in component that coordinates loading states for async components in your component tree. Instead of managing separate isLoading flags in every nested child, <Suspense> waits for async dependencies to resolve before showing the final UI.
It works with two kinds of async dependencies: components that use top-level await inside <script setup>, and lazy components created with defineAsyncComponent(). While those dependencies load, <Suspense> shows a fallback skeleton or spinner.
flowchart TD
Suspense["Suspense Boundary"]
Suspense -->|Pending| Fallback["#fallback Slot (Loading UI)"]
Suspense -->|Resolved| Default["#default Slot (Async Component)"]
#default and #fallback Slots#default Slot: Holds the component tree containing one or more async child components.#fallback Slot: Renders immediately while any async dependency inside #default is still pending.First, create AsyncDataWidget.vue with top-level await in <script setup>. Vue automatically marks this component as async.
<script setup>
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const post = await response.json();
</script>
<template>
<article class="widget">
<h4>{{ post.title }}</h4>
<p>{{ post.body }}</p>
</article>
</template>
Next, wrap AsyncDataWidget in <Suspense> inside App.vue and pair it with onErrorCaptured() so failed requests show a clear error message.
<script setup>
import { ref, onErrorCaptured } from 'vue';
import AsyncDataWidget from './components/AsyncDataWidget.vue';
const error = ref(null);
onErrorCaptured((err) => {
error.value = err.message;
return false;
});
</script>
<template>
<main>
<h2>Dashboard Summary</h2>
<div v-if="error" class="error-box">
Failed to load widget: {{ error }}
</div>
<Suspense v-else>
<template #default>
<AsyncDataWidget />
</template>
<template #fallback>
<div class="skeleton-loader">
Loading widget data...
</div>
</template>
</Suspense>
</main>
</template>
<Suspense> handles loading states, not rejected promises. Always wrap it with onErrorCaptured() in the parent component to catch network errors.#default: Both #default and #fallback slots expect a single root node (which can be a wrapper element or single component).Suspense Is Experimental: While widely used in Vue 3 apps and Nuxt, the <Suspense> API is still documented as experimental in Vue 3 core.<Suspense> inside a child component?<Suspense> activates when a descendant has an async setup() function, uses top-level await inside <script setup>, or is loaded via defineAsyncComponent() without disabling suspensible mode.
Wait-state behavior can be tuned with timeout or by keeping existing UI mounted inside KeepAlive. By default, once resolved, <Suspense> only shows #fallback again if the root node inside #default is replaced.
<Suspense> work together with Vue Router?Yes. Combine <RouterView v-slot="{ Component }"> with <Suspense> and <KeepAlive> to display route-level loading states cleanly.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With
You've completed this section! Take a quick 5-question quiz to check your understanding.