---
title: "Async tests: nextTick, flushPromises and fake timers · vuejs.school"
description: "Why a component test sees stale DOM, and which await fixes it, from nextTick and flushPromises to fake timers, vi.waitFor and async setup."
canonical_url: "https://vuejs.school/community/blog/async-tests-nexttick-flushpromises-and-fake-timers"
meta:
  "og:description": "Why a component test sees stale DOM, and which await fixes it, from nextTick and flushPromises to fake timers, vi.waitFor and async setup."
  "og:image": "https://vuejs.school/_og/d/a_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycw%2Cdescription_~V2h5IGEgY29tcG9uZW50IHRlc3Qgc2VlcyBzdGFsZSBET00sIGFuZCB3aGljaCBhd2FpdCBmaXhlcyBpdCwgZnJvbSBuZXh0VGljayBhbmQgZmx1c2hQcm9taXNlcyB0byBmYWtlIHRpbWVycywgdmkud2FpdEZvciBhbmQgYXN5bmMgc2V0dXAu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hc3luYy10ZXN0cy1uZXh0dGljay1mbHVzaHByb21pc2VzLWFuZC1mYWtlLXRpbWVycyI%2Cs_oHsp6TrpoOq_GSLY.png"
  "og:image:alt": "Async tests: nextTick, flushPromises and fake timers · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Async tests: nextTick, flushPromises and fake timers · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycw%2Cdescription_~V2h5IGEgY29tcG9uZW50IHRlc3Qgc2VlcyBzdGFsZSBET00sIGFuZCB3aGljaCBhd2FpdCBmaXhlcyBpdCwgZnJvbSBuZXh0VGljayBhbmQgZmx1c2hQcm9taXNlcyB0byBmYWtlIHRpbWVycywgdmkud2FpdEZvciBhbmQgYXN5bmMgc2V0dXAu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hc3luYy10ZXN0cy1uZXh0dGljay1mbHVzaHByb21pc2VzLWFuZC1mYWtlLXRpbWVycyI%2Cs_oHsp6TrpoOq_GSLY.png"
  "twitter:image:alt": "Async tests: nextTick, flushPromises and fake timers · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Why the DOM lags behind your state

When reactive state changes, Vue does not re-render on the spot. It puts the component in a queue and renders everything in that queue once, in a **microtask**: a small job the JavaScript engine runs as soon as the current code finishes, before anything else happens. That batching is why ten changes in a row cost one render. It is also why this test fails:

*src/components/Counter.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <p>Count: {{ count }}</p>
  <button type="button" @click="count++">Add one</button>
</template>
```

*src/components/Counter.spec.ts**Breaks***

```
import { mount } from '@vue/test-utils'
import { expect, it } from 'vitest'
import Counter from './Counter.vue'

it('adds one', () => {
  const wrapper = mount(Counter)
  wrapper.find('button').trigger('click')
  expect(wrapper.text()).toContain('Count: 1')
})
```

Vitest reports `expected 'Count: 0Add one' to contain 'Count: 1'`. The click ran and `count` is 1, but the render is still queued. Vue's `nextTick()` returns a promise that resolves after the queued render, and Vue Test Utils' `trigger`, `setValue` and `setProps` return that promise for you. So the fix is one word:

*src/components/Counter.spec.ts**Works***

```
it('adds one', async () => {
  const wrapper = mount(Counter)
  await wrapper.find('button').trigger('click')
  expect(wrapper.text()).toContain('Count: 1')
})
```

When you change state without going through the wrapper, for example by writing to a Pinia store or to a ref returned by a composable, nothing hands you a promise. Write `await nextTick()` yourself, imported from `vue`.

## flushPromises after a fetch resolves

`nextTick` only waits for Vue's render queue. It knows nothing about your own promises. This component loads a name when it mounts:

*src/components/UserName.vue*

```
<script setup lang="ts">
import { onMounted, ref } from 'vue'

const name = ref<string>()

onMounted(async () => {
  const response = await fetch('/api/user')
  const user: { name: string } = await response.json()
  name.value = user.name
})
</script>

<template>
  <p v-if="name">Hello, {{ name }}</p>
  <p v-else>Loading…</p>
</template>
```

With `fetch` replaced by a mock that resolves at once, `await nextTick()` is still not enough. Two promises have to settle (the response, then its JSON) before `name` changes, and only then is a render queued. `nextTick` resolved long before that.

*src/components/UserName.spec.ts**Works***

```
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, expect, it, vi } from 'vitest'
import UserName from './UserName.vue'

afterEach(() => {
  vi.unstubAllGlobals()
})

it('greets the user once the request resolves', async () => {
  vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: async () => ({ name: 'Ada' }) }))
  const wrapper = mount(UserName)
  await flushPromises()
  expect(wrapper.text()).toContain('Hello, Ada')
})
```

`flushPromises()` waits for one **macrotask**, the next turn of the event loop. By then every promise callback that was already queued has run, including the ones those callbacks queued, and so has Vue's render. What it cannot do is wait for a real timer or a real network request. Those are not in the queue yet.

## Fake timers for debounce and setTimeout

A search box that waits 300 ms after the last keystroke should not make your test wait 300 ms. Fake timers replace `setTimeout` and its relatives with a clock you move by hand. In Vitest 4.1, `vi.useFakeTimers()` fakes every timer API it finds (`setTimeout`, `setInterval`, `setImmediate`, `Date` and the rest) but leaves Node's `process.nextTick` and `queueMicrotask` alone. Promises are not timers, so they still resolve on their own, and Vue's `nextTick` keeps working because it is built on a promise.

*src/components/SearchBox.vue*

```
<script setup lang="ts">
import { ref, watch } from 'vue'

const emit = defineEmits<{ search: [query: string] }>()
const query = ref('')
let timer: ReturnType<typeof setTimeout> | undefined

watch(query, (value) => {
  clearTimeout(timer)
  timer = setTimeout(() => emit('search', value.trim()), 300)
})
</script>

<template>
  <label>Search <input v-model="query" type="search"></label>
</template>
```

*src/components/SearchBox.spec.ts**Works***

```
import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import SearchBox from './SearchBox.vue'

beforeEach(() => {
  vi.useFakeTimers()
})
afterEach(() => {
  vi.useRealTimers()
})

it('emits once, 300 ms after the last keystroke', async () => {
  const wrapper = mount(SearchBox)
  const input = wrapper.find('input')
  await input.setValue('v')
  await input.setValue('vue')

  vi.advanceTimersByTime(299)
  expect(wrapper.emitted('search')).toBeUndefined()

  vi.advanceTimersByTime(1)
  expect(wrapper.emitted('search')).toEqual([['vue']])
})
```

The two `advanceTimersByTime` calls move the clock 299 ms, one short of the delay, and then the last millisecond, so the test checks both the wait and the emit.

Drop the `await` before `setValue` and the test fails with `expected undefined to deeply equal [ [ 'vue' ] ]`. A `watch` callback runs in Vue's queue too, so without the await the timer has not been started when you move the clock, and there is nothing to fire.

Always restore real timers in `afterEach`. A test that leaves fake timers on hands the frozen clock to every test that runs after it in the same file.

## Timers and promises together need the Async methods

The hard cases mix both. This component retries once, a second after a failed request:

*src/components/RetryingUser.vue*

```
<script setup lang="ts">
import { onMounted, ref } from 'vue'

const props = defineProps<{ fetchUser: () => Promise<{ name: string }> }>()
const name = ref<string>()

onMounted(async () => {
  try {
    name.value = (await props.fetchUser()).name
  }
  catch {
    await new Promise(resolve => setTimeout(resolve, 1000))
    name.value = (await props.fetchUser()).name
  }
})
</script>

<template>
  <p>{{ name ?? 'Loading…' }}</p>
</template>
```

*src/components/RetryingUser.spec.ts**Breaks***

```
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import RetryingUser from './RetryingUser.vue'

beforeEach(() => {
  vi.useFakeTimers()
})
afterEach(() => {
  vi.useRealTimers()
})

it('shows the user after one retry', async () => {
  const fetchUser = vi.fn<() => Promise<{ name: string }>>()
    .mockRejectedValueOnce(new Error('offline'))
    .mockResolvedValueOnce({ name: 'Ada' })
  const wrapper = mount(RetryingUser, { props: { fetchUser } })
  vi.advanceTimersByTime(1000)
  await flushPromises()
  expect(wrapper.text()).toBe('Ada')
})
```

It fails with `expected 'Loading…' to be 'Ada'`. `advanceTimersByTime` runs synchronously, straight after `mount`, before the rejected promise has been handled. At that moment the one-second timer does not exist, so nothing fires.

The async variant lets pending promises run before and between timers, so it also fires a timer that is only set once a promise settles (the Vitest docs call these "asynchronously set timers"). In the test, one awaited call replaces `advanceTimersByTime` and `flushPromises`:

*src/components/RetryingUser.spec.ts**Works***

```
const wrapper = mount(RetryingUser, { props: { fetchUser } })
await vi.advanceTimersByTimeAsync(1000)
expect(wrapper.text()).toBe('Ada')
```

Use the `Async` versions (`advanceTimersByTimeAsync`, `runAllTimersAsync`, `advanceTimersToNextTimerAsync`) whenever promises and timers meet.

The other mistake when the two meet is a home-made pause inside the test, such as `await new Promise(r => setTimeout(r, 1100))`. With fake timers on, that `setTimeout` is fake too. Nothing advances it, so the test hangs until Vitest's timeout.

`flushPromises` waits on a timer as well, but it does not hang. Vue Test Utils keeps a reference to `setImmediate` from the moment it loads, and the test file imports it before `vi.useFakeTimers()` runs, so that reference is the real one.

## vi.waitFor when you cannot count the ticks

Sometimes you do not control how many steps sit between the action and the result, for example inside a third-party library. `vi.waitFor` runs a callback until it stops throwing, checking every 50 ms for up to one second by default. In the `UserName` test above, with the same `fetch` stub, it would replace `flushPromises` like this:

*src/components/UserName.spec.ts*

```
const wrapper = mount(UserName)
await vi.waitFor(() => expect(wrapper.text()).toContain('Hello, Ada'))
```

It is honest about uncertainty, but slower and vaguer than an exact await, so keep it for those cases.

Be careful combining it with fake timers. The docs say it then advances the fake clock by one interval on each check, so the defaults cover only about a second of fake time. The retry test above, which needs exactly a second, times out that way. Move the clock explicitly instead.

## Suspense and async setup

A component with a top-level `await` in `<script setup>` has an async setup, and Vue only renders it inside `<Suspense>`:

*src/components/UserCard.vue*

```
<script setup lang="ts">
const props = defineProps<{ load: () => Promise<{ name: string }> }>()
const user = await props.load()
</script>

<template>
  <h2>{{ user.name }}</h2>
</template>
```

Mount it directly and the wrapper stays empty while Vue warns: `setup function returned a promise, but no <Suspense> boundary was found in the parent component tree`. Give it the boundary it would have in your app, then flush:

*src/components/UserCard.spec.ts**Works***

```
import { flushPromises, mount } from '@vue/test-utils'
import { expect, it, vi } from 'vitest'
import { defineComponent, h, Suspense } from 'vue'
import UserCard from './UserCard.vue'

it('renders the loaded user', async () => {
  const load = vi.fn().mockResolvedValue({ name: 'Ada' })
  const wrapper = mount(defineComponent({
    render: () => h(Suspense, null, { default: () => h(UserCard, { load }) }),
  }))
  await flushPromises()
  expect(wrapper.text()).toContain('Ada')
})
```

The render function avoids a string template, which would need Vue's runtime compiler in the test build. To reach the inner component, use `wrapper.findComponent(UserCard)`. In a Nuxt project, `mountSuspended` from `@nuxt/test-utils` does this wrapping for you.

## Match the failure to the missing wait

The failure message usually tells you which kind of "later" you missed:

- **Old text right after a click or an input.** An `await` is missing before `trigger` or `setValue`, or state changed outside the wrapper and needs `await nextTick()`.
- **A loading state that never goes away with a mocked request.** The promise has not settled yet. Add `await flushPromises()`.
- **A debounced or delayed effect that never happens.** Turn on fake timers, and check that the action which starts the timer was awaited before you move the clock.
- **A timer and a promise in the same flow.** Switch to `advanceTimersByTimeAsync`.
- **An empty wrapper and a Suspense warning.** The component has an async setup. Mount it inside `<Suspense>`.
- **A test that times out after exactly five seconds with fake timers on.** Five seconds is Vitest's default test timeout, and something is waiting on a fake timer that nothing advances, often a home-made `setTimeout` pause in the test.

**Check it stuck**3 questions from the Testing async code and mocks quiz.

[**Practice now**](https://vuejs.school/learn/testing/testing-async-code-and-mocks?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 6 min read**Vitest 5: what changed for Vue tests**](https://vuejs.school/community/blog/vitest-5-what-changed-for-vue-tests) [Release notes · 6 min read**Vitest 5 release candidate: what's coming for Vue tests**](https://vuejs.school/community/blog/vitest-5-rc-whats-coming)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/async-tests-nexttick-flushpromises-and-fake-timers#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-08-06","description":"Why a component test sees stale DOM, and which await fixes it, from nextTick and flushPromises to fake timers, vi.waitFor and async setup.","name":"Async tests: nextTick, flushPromises and fake timers","url":"https://vuejs.school/community/blog/async-tests-nexttick-flushpromises-and-fake-timers","breadcrumb":{"@id":"https://vuejs.school/community/blog/async-tests-nexttick-flushpromises-and-fake-timers#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/async-tests-nexttick-flushpromises-and-fake-timers"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/async-tests-nexttick-flushpromises-and-fake-timers#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-08-06","description":"Why a component test sees stale DOM, and which await fixes it, from nextTick and flushPromises to fake timers, vi.waitFor and async setup.","headline":"Async tests: nextTick, flushPromises and fake timers","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycw%2Cdescription_~V2h5IGEgY29tcG9uZW50IHRlc3Qgc2VlcyBzdGFsZSBET00sIGFuZCB3aGljaCBhd2FpdCBmaXhlcyBpdCwgZnJvbSBuZXh0VGljayBhbmQgZmx1c2hQcm9taXNlcyB0byBmYWtlIHRpbWVycywgdmkud2FpdEZvciBhbmQgYXN5bmMgc2V0dXAu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hc3luYy10ZXN0cy1uZXh0dGljay1mbHVzaHByb21pc2VzLWFuZC1mYWtlLXRpbWVycyI%2Cs_oHsp6TrpoOq_GSLY.png","@type":["Article","BlogPosting"],"articleSection":["Deep dive"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/async-tests-nexttick-flushpromises-and-fake-timers#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/async-tests-nexttick-flushpromises-and-fake-timers#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/async-tests-nexttick-flushpromises-and-fake-timers#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Async tests: nextTick, flushPromises and fake timers","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycw%2Cdescription_~V2h5IGEgY29tcG9uZW50IHRlc3Qgc2VlcyBzdGFsZSBET00sIGFuZCB3aGljaCBhd2FpdCBmaXhlcyBpdCwgZnJvbSBuZXh0VGljayBhbmQgZmx1c2hQcm9taXNlcyB0byBmYWtlIHRpbWVycywgdmkud2FpdEZvciBhbmQgYXN5bmMgc2V0dXAu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hc3luYy10ZXN0cy1uZXh0dGljay1mbHVzaHByb21pc2VzLWFuZC1mYWtlLXRpbWVycyI%2Cs_oHsp6TrpoOq_GSLY.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~QXN5bmMgdGVzdHM6IG5leHRUaWNrLCBmbHVzaFByb21pc2VzIGFuZCBmYWtlIHRpbWVycw%2Cdescription_~V2h5IGEgY29tcG9uZW50IHRlc3Qgc2VlcyBzdGFsZSBET00sIGFuZCB3aGljaCBhd2FpdCBmaXhlcyBpdCwgZnJvbSBuZXh0VGljayBhbmQgZmx1c2hQcm9taXNlcyB0byBmYWtlIHRpbWVycywgdmkud2FpdEZvciBhbmQgYXN5bmMgc2V0dXAu%2Ceyebrow_~RGVlcCBkaXZlIMK3IDYgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hc3luYy10ZXN0cy1uZXh0dGljay1mbHVzaHByb21pc2VzLWFuZC1mYWtlLXRpbWVycyI%2Cs_oHsp6TrpoOq_GSLY.png"}]}
```