Because Vue only auto-cleans watchers that are created synchronously during setup(). When you create a watch or watchEffect inside a setTimeout, Promise.then, or after an await, Vue can't bind it to the component lifecycle. It keeps running after the component unmounts.
onMounted(async () => {
await loadInitialData()
// This watcher is NOT bound to the component
watch(data, (newVal) => {
processData(newVal) // keeps running after unmount
})
})Same problem with setTimeout:
onMounted(() => {
setTimeout(() => {
watchEffect(() => {
console.log(data.value) // keeps running after unmount
})
}, 1000)
})How to fix it
Option 1 (preferred): Create the watcher synchronously with conditional logic inside.
const config = ref(null)
const userData = ref(null)
// Created synchronously, auto-cleaned on unmount
watch(userData, (newData) => {
if (config.value && newData) {
applySettings(config.value, newData)
}
})
onMounted(async () => {
config.value = await fetchConfig()
})Option 2: Store the stop function and call it manually on unmount.
let stopWatcher: (() => void) | null = null
onMounted(async () => {
await loadData()
stopWatcher = watch(data, (newVal) => {
processData(newVal)
})
})
onUnmounted(() => {
stopWatcher?.()
})The first option is almost always better. If you can restructure the logic so the watcher is created synchronously and the async condition is checked inside the callback, you avoid the manual cleanup entirely.
See also: Why does my watchEffect miss dependencies after an await? · What is effectScope and when would you use it?