Vue 3's `<Suspense>` is still labelled experimental: what does that mean in practice, and how would you use it safely in a production app?
answer
- the docs warning and a dev notice
- API may change, not broken today
- no error slot of its own
- contain it behind one wrapper
basics
~20 sExperimental means the Vue team does not guarantee the API will stay as it is or reach stable status; it works today and keeps receiving fixes. Use it behind a small wrapper component, handle errors in an ancestor, and track release notes.
solid answer
~50 sThe Vue guide opens the Suspense page with a warning that it is **experimental**, not guaranteed to reach stable status, and that its API may change; in development Vue also logs `<Suspense> is an experimental feature and its API will likely change.` once. That is a stability promise about the API, not a statement that it is buggy. The practical gaps: it has **no error slot**, so a rejected async setup must be caught with an error-capturing hook in an ancestor; route components loaded lazily by Vue Router do not trigger it by themselves; and props, events and nesting behaviour could change. A safe approach is to put Suspense in one shared wrapper component with the fallback and error handling, keep async setup to components that truly need it, and review changelog entries on upgrades.
go deeper
Know that Suspense is officially experimental in Vue 3 and that its API may still change.
Explain the practical gaps: no error slot, error capturing in a parent, and lazy routes that do not trigger it on their own.
Propose containment: one wrapper with fallback and error handling, async setup only where coordination pays off, and tests that pin the behaviour you rely on.
Frame adoption as a risk budget: accept an experimental API where coordinated loading clearly helps users, and keep an exit path if it changes.
## What "experimental" means here Vue's documentation labels `<Suspense>` experimental in two places: the API reference badge and a warning at the top of the guide page, which says it **is not guaranteed to reach stable status and the API may change before it does**. In development builds, the first boundary Vue creates also prints a console notice: `<Suspense> is an experimental feature and its API will likely change.` It has been part of Vue 3 since the start and receives fixes like the rest of the runtime. The label is about **API stability**: prop names, event semantics and nesting behaviour are not frozen the way the stable APIs are. ## Known gaps to plan around - **No error handling of its own.** Suspense has no error slot. The guide points to the `errorCaptured` option or `onErrorCaptured()` in the **parent of `<Suspense>`** to handle errors from async dependencies. Without that, a rejected top-level `await` goes to app-level error handling, and Vue still tries to render the failed component's template, which usually produces further errors. - **Lazy route components.** The Vue guide notes that Vue Router's lazily loaded route components, which use dynamic imports, are distinct from async components and do **not** trigger Suspense by themselves; async components or async setup inside them still do. - **Evolving details.** Behaviour such as nested boundaries needed the `suspensible` prop added in 3.3; similar refinements can arrive in minor versions. - **Interaction with other built-ins** depends on nesting order, which the guide prescribes. ## Should you use it in production? A reasonable answer weighs benefit against risk rather than saying yes or no: | Consideration | Favors using it | Favors avoiding it | |---|---|---| | UX need | many async pieces must appear together | independent widgets can load separately | | Code style | top-level `await` keeps setup code linear | explicit loading flags are already in place | | Change risk | usage is contained in one place | usage is spread across dozens of pages | | Error handling | a shared ancestor handles errors | errors must be handled per widget | ## How to contain it 1. **One wrapper component**, for example `AsyncBoundary.vue`, renders `<Suspense>` with a standard fallback and registers `onErrorCaptured` to show an error state. Pages use the wrapper, never `<Suspense>` directly, so an API change touches one file. 2. **Async setup only where coordination pays off.** For data a component can show progressively, start the request without awaiting and render a local loading state. 3. **Tests for the boundary behaviour you rely on**: fallback shown on first load, old content kept on switch, error state on rejection. A minor upgrade that changes behaviour then fails a test instead of production. 4. **Read the changelog** for Suspense entries when upgrading minor versions. ## Alternatives if you decide against it Choosing not to adopt Suspense does not mean losing coordinated loading entirely: - **A data composable** that returns `data`, `pending` and `error` refs lets each component render its own states with stable APIs. - **Lifting requests** into a parent and passing results down as props gives one loading state for a group of children, with `v-if` on the parent's `pending` flag. - **Async components' own options**, such as a loading component and an error component, cover lazily loaded code without a boundary. These cost more template code than top-level `await` under a boundary, but every piece is a stable, documented API. ## A wrapper sketch ```vue <script setup lang="ts"> import { ref, onErrorCaptured } from 'vue' const error = ref<unknown>(null) onErrorCaptured(e => { error.value = e return false // stop further propagation }) </script> <template> <p v-if="error" role="alert">Something went wrong.</p> <Suspense v-else> <slot /> <template #fallback><slot name="fallback">Loading...</slot></template> </Suspense> </template> ``` The error hook sits in the parent of the boundary, as the guide recommends; the detailed error-handling semantics belong to Vue's error-handling APIs. One limit of this wrapper: the root of the boundary's `#default` is the slot's fragment, which stays the same node when the page inside switches views. A resolved boundary only goes pending again when that root is replaced, so the wrapper suits a page's first load. For switching views, put the dynamic component directly inside a `<Suspense>`. ## What interviewers listen for - Knowing it **is** experimental, and what the label does and does not mean. - The concrete gaps: no error slot, router lazy routes not triggering it. - A containment strategy rather than a blanket yes or no.
- How do you handle a rejected top-level await inside a Vue 3 <Suspense> subtree?Suspense has no error slot, so register `onErrorCaptured` (or the `errorCaptured` option) in a component that is the parent of the `<Suspense>`, switch to an error state there, and return `false` if the error should not propagate further. Without such a handler, the error goes up to app-level error handling, and Vue still tries to render the failed component's template, which usually adds further errors.
- Why might a lazily loaded route not show your Vue 3 <Suspense> fallback?The Vue guide notes that Vue Router's lazily loaded route components use dynamic imports that are distinct from async components and do not trigger Suspense. Only async components or components with async setup inside the route will register as dependencies.
saying these in an interview costs you the question
- Experimental means Suspense is broken and unusable in production
- Suspense has an #error slot for rejected async setup
- Every lazily loaded route component triggers the Suspense fallback
- The experimental label was removed in Vue 3.3
- Using Suspense directly on every page keeps upgrades easy