Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/composition-api-script-setup-async-context.md
1---2title: Top-Level await in script setup Preserves Component Context3impact: HIGH4impactDescription: Misunderstanding async context causes lifecycle hooks and watchers to silently fail5type: gotcha6tags: [vue3, composition-api, script-setup, async, await, suspense]7---89# Top-Level await in script setup Preserves Component Context1011**Impact: HIGH** - In `<script setup>`, top-level `await` statements preserve component context (allowing lifecycle hooks and watchers after `await`), but this is a special case. Nested async functions or callbacks lose context, causing lifecycle hooks to silently fail.1213Vue's compiler automatically injects context restoration after each top-level await in `<script setup>`. This doesn't apply to `setup()` function or nested async contexts.1415## Task Checklist1617- [ ] Understand that top-level await in `<script setup>` is specially handled18- [ ] Never register lifecycle hooks in nested async functions19- [ ] Use `<Suspense>` when using async `<script setup>` components20- [ ] In regular `setup()`, never use await before lifecycle hook registration21- [ ] Register hooks synchronously, then do async work inside them2223**Top-Level await Works (script setup only):**24```vue25<script setup>26import { ref, onMounted, watch } from 'vue'2728// This is TOP-LEVEL await - Vue compiler preserves context29const config = await fetchConfig() // OK!3031// These hooks work because Vue restored context32onMounted(() => {33console.log('This will run!') // Works34})3536watch(someRef, () => {37console.log('This will track!') // Works38})3940// Another top-level await - still OK41const data = await fetchData(config.apiUrl) // OK!4243// Still works after multiple awaits44onMounted(() => {45console.log('This also runs!') // Works46})47</script>4849<!-- IMPORTANT: Parent must use Suspense -->50<template>51<Suspense>52<AsyncComponent />53</Suspense>54</template>55```5657**Nested Async Breaks Context:**58```vue59<script setup>60import { ref, onMounted, watch } from 'vue'6162// WRONG: Nested async function - context lost after await63async function initializeData() {64const config = await fetchConfig()6566// BUG: This hook will NOT be registered!67// We're no longer in the synchronous setup context68onMounted(() => {69console.log('This will NEVER run!') // Silent failure70})7172// BUG: This watcher won't auto-dispose on unmount73watch(someRef, () => {74console.log('Memory leak - not cleaned up!')75})76}7778// Calling the async function79initializeData() // Hooks inside won't work!8081// WRONG: Callbacks also lose context82setTimeout(async () => {83await delay(100)84onMounted(() => {85console.log('Never runs!') // Silent failure86})87}, 0)88</script>89```9091**Correct Patterns:**92```vue93<script setup>94import { ref, onMounted, watch } from 'vue'9596const data = ref(null)97const config = ref(null)9899// CORRECT: Register hooks synchronously FIRST100onMounted(async () => {101// Then do async work INSIDE the hook102config.value = await fetchConfig()103data.value = await fetchData(config.value.apiUrl)104})105106// CORRECT: Watchers registered synchronously107watch(config, async (newConfig) => {108if (newConfig) {109data.value = await fetchData(newConfig.apiUrl)110}111})112113// Or use top-level await for initial data114const initialConfig = await fetchConfig() // OK - top level115config.value = initialConfig116117onMounted(() => {118console.log('Works!') // Context preserved by compiler119})120</script>121```122123**setup() Function (Not script setup):**124```javascript125// In regular setup(), await ALWAYS breaks context126export default {127async setup() {128const data = ref(null)129130// WRONG: Hooks after await won't register131const config = await fetchConfig()132133onMounted(() => {134console.log('Never runs!') // Silent failure!135})136137return { data }138}139}140141// CORRECT: Register hooks before any await142export default {143async setup() {144const data = ref(null)145146// Register hooks FIRST (synchronous)147onMounted(async () => {148const config = await fetchConfig()149data.value = await fetchData(config)150})151152// Now you can await if needed153// But hooks must be registered before this point154155return { data }156}157}158```159160## Why This Happens161162```javascript163// Vue tracks the "current component instance" during setup164// This is like a global variable that gets set and cleared165166// During synchronous setup:167function setup() {168currentInstance = this // Vue sets this169170onMounted(cb) // Uses currentInstance to register171172// After await, JavaScript resumes in a microtask173await something()174175// currentInstance is now null or different!176onMounted(cb) // Can't find the instance - silently fails177}178179// <script setup> compiler adds restoration:180// After each await, it injects: setCurrentInstance(savedInstance)181```182183## Suspense Requirement184185```vue186<!-- When using async script setup, parent needs Suspense -->187<template>188<Suspense>189<!-- Async component with top-level await -->190<AsyncChild />191192<!-- Optional: Loading state -->193<template #fallback>194<LoadingSpinner />195</template>196</Suspense>197</template>198```199200## Reference201- [Composition API FAQ - Async Setup](https://vuejs.org/guide/extras/composition-api-faq.html)202- [Composables - Async Without Await](https://antfu.me/posts/async-with-composition-api)203- [Suspense](https://vuejs.org/guide/built-ins/suspense.html)204