---
title: "A debugging checklist for Vue apps · vuejs.school"
description: "Read the warning, open DevTools, set a breakpoint and shrink it in the playground: a fixed order for finding Vue bugs, with the warnings you will meet most."
canonical_url: "https://vuejs.school/community/blog/a-debugging-checklist-for-vue-apps"
meta:
  "og:description": "Read the warning, open DevTools, set a breakpoint and shrink it in the playground: a fixed order for finding Vue bugs, with the warnings you will meet most."
  "og:image": "https://vuejs.school/_og/d/a_~QSBkZWJ1Z2dpbmcgY2hlY2tsaXN0IGZvciBWdWUgYXBwcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_A%2Bdebugging%2Bchecklist%2Bfor%2BVue%2Bapps%2Cdescription_~UmVhZCB0aGUgd2FybmluZywgb3BlbiBEZXZUb29scywgc2V0IGEgYnJlYWtwb2ludCBhbmQgc2hyaW5rIGl0IGluIHRoZSBwbGF5Z3JvdW5kOiBhIGZpeGVkIG9yZGVyIGZvciBmaW5kaW5nIFZ1ZSBidWdzLCB3aXRoIHRoZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IG1vc3Qu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hLWRlYnVnZ2luZy1jaGVja2xpc3QtZm9yLXZ1ZS1hcHBzIg%2Cs_rYwWZEaqaHo02mTG.png"
  "og:image:alt": "A debugging checklist for Vue apps · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "A debugging checklist for Vue apps · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~QSBkZWJ1Z2dpbmcgY2hlY2tsaXN0IGZvciBWdWUgYXBwcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_A%2Bdebugging%2Bchecklist%2Bfor%2BVue%2Bapps%2Cdescription_~UmVhZCB0aGUgd2FybmluZywgb3BlbiBEZXZUb29scywgc2V0IGEgYnJlYWtwb2ludCBhbmQgc2hyaW5rIGl0IGluIHRoZSBwbGF5Z3JvdW5kOiBhIGZpeGVkIG9yZGVyIGZvciBmaW5kaW5nIFZ1ZSBidWdzLCB3aXRoIHRoZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IG1vc3Qu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hLWRlYnVnZ2luZy1jaGVja2xpc3QtZm9yLXZ1ZS1hcHBzIg%2Cs_rYwWZEaqaHo02mTG.png"
  "twitter:image:alt": "A debugging checklist for Vue apps · vuejs.school"
---

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

## Read the warning first

Vue's development build prints a `[Vue warn]` for most mistakes, with the component stack underneath. Read it to the end before touching anything. These four are worth knowing by sight.

**Extraneous non-props attributes.** The full text is `Extraneous non-props attributes (class) were passed to component but could not be automatically inherited because component renders fragment or text or teleport root nodes.` It means you wrote `<FieldLabel class="mt-2" />` and the component has more than one root element, so Vue did not know which one should receive the class, and dropped it. Here `FieldLabel` renders a `<label>` and a `<small>`:

*src/components/FieldLabel.vue**Breaks***

```
<script setup lang="ts">
defineProps<{ label: string; hint: string }>()
</script>

<template>
  <label>{{ label }}</label>
  <small>{{ hint }}</small>
</template>
```

The fix is to choose the element yourself:

*src/components/FieldLabel.vue**Works***

```
<script setup lang="ts">
defineOptions({ inheritAttrs: false })
defineProps<{ label: string; hint: string }>()
</script>

<template>
  <label v-bind="$attrs">{{ label }}</label>
  <small>{{ hint }}</small>
</template>
```

`inheritAttrs: false` turns off the automatic forwarding, and `v-bind="$attrs"` places the class, the id and any listeners on the element you picked.

**Missing or duplicate keys.** A `v-for` without a `:key` gets no runtime warning; the lint rule `vue/require-v-for-key` from eslint-plugin-vue catches it before the code runs. Without keys Vue reuses rows by position, so after a sort a checkbox can stay ticked on the wrong row. Vue does warn when two items share a key and the list updates: `Duplicate keys found during update:`, then the key, then `Make sure keys are unique.` The key is usually a field that is not unique, or two lists were joined. Key on a real id.

**Mutating a prop.** `Set operation on key "count" failed: target is readonly.` You wrote `props.count++`. Props flow down; to change the value, emit an event and let the parent change it, or copy the prop into a local `ref` if the component really owns the edit.

**Hydration mismatch.** This one appears only in server-rendered apps such as Nuxt. While hydrating (taking over the server's HTML), the browser computed a different value from the server's, and Vue replaced the server's text with its own. The warning names what differed, for example `Hydration text content mismatch on` and the element, then `rendered on server:` and `expected on client:` with both values.

The usual causes are the current time, `Math.random()`, or something only the browser knows, such as `window.innerWidth` or a value in `localStorage`. Read those in `onMounted()`, which runs in the browser only, or wrap the markup in Nuxt's `<ClientOnly>`. This Nuxt component greets by the hour without a mismatch:

*app/components/Greeting.vue**Works***

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

const hour = ref<number | null>(null)

onMounted(() => {
  hour.value = new Date().getHours()
})
</script>

<template>
  <p v-if="hour === null">Hello</p>
  <p v-else>{{ hour < 12 ? 'Good morning' : 'Good afternoon' }}</p>
</template>
```

Server and browser both render "Hello" first; the greeting appears once the component is mounted.

## Check the state in Vue DevTools

Vue DevTools comes as a browser extension, which adds a Vue panel to the developer tools, and as the `vite-plugin-vue-devtools` package, which draws the same panels inside your page during `vite dev`. The extension needs nothing from your project beyond a development build: a production build leaves out the code that connects Vue to DevTools, so the panels stay empty on your live site.

Version 8.0 was published on 27 July. Its [release notes](https://github.com/vuejs/devtools/releases/tag/v8.0.0) list two breaking changes, both in the Vite plugin: Vite versions below 6 are no longer supported, and the bundled `vite-plugin-inspect` was bumped.

Two panels do most of the work. **Components** shows the tree as Vue sees it, not the DOM. Click a component and the pane beside it lists its props, its setup state, its computed values and anything it injected, live. Edit a value there and the page updates. That answers the first real question: is the state wrong, or is the template ignoring it? The inspector button lets you click an element on the page to jump to its component, and the open-in-editor button opens the file.

**Timeline** records what happened, in order, in four layers: mouse, keyboard, component events and performance. It records nothing until you press its record button and enable the layers you need (recording slows a large app). The performance layer lists each render with its component and duration, so something that updates twice shows as two entries, and a slow form shows which render took the time.

## Log the value, not the ref

`console.log(count)` prints `RefImpl {…}` with the number buried in `_value`, which makes a working ref look broken. Log `count.value`. For a reactive object, `console.log(state)` prints a `Proxy`, and worse, the console shows the object as it is when you expand it, not as it was when you logged it. Unwrap and snapshot:

```
import { toRaw } from 'vue'

console.log(count.value)
console.log(structuredClone(toRaw(state)))
```

`toRaw()` returns the plain object behind the proxy; `structuredClone()` copies it at that instant (it refuses objects that hold functions). For something that changes over time, a `watch` with the values in the callback beats scattered logs, because it tells you what changed and when. In a search component with `query` and `results` refs:

```
import { watch } from 'vue'

watch(
  () => [query.value, results.value.length] as const,
  ([q, n]) => console.log(`query ${JSON.stringify(q)} gave ${n} results`),
)
```

Chrome's console can also show refs unwrapped: enable custom formatters in the developer tools settings, and Vue's development build registers one.

## Pause the code with a breakpoint

Vite serves source maps in development, so the browser's Sources panel shows your `.vue` files rather than the compiled output. Press Cmd+P (Ctrl+P on Windows and Linux) inside the developer tools, type the component's file name, and click a line number in the `<script setup>` block to set a breakpoint. Or write `debugger` on the line; it stops there whenever the developer tools are open:

```
function addToCart(item: Product) {
  debugger
  cart.add(item)
}
```

Stop on the first line of a handler to confirm it runs at all, and with the arguments you expect. Step with F10 (over) and F11 (into) and keep an eye on the Scope pane. A breakpoint answers in one minute what ten logs answer in ten.

You can also stop inside a `watch` callback or a `computed`, but the top of the call stack will be Vue's code: Vue runs a watch callback later, from its own queue, and recalculates a computed when something reads it, usually the next render. Below a line marked `Promise.then (async)`, Chrome shows the change that started that queue. When a handler changes several values, that can be an earlier one than the change your watcher follows.

The `onTrigger` option runs at the moment of the change itself, so a `debugger` there has the line that made it on the call stack. Here it is on a watcher that runs `search()` when `query` changes:

```
watch(query, search, {
  onTrigger(event) {
    debugger // event.key and event.newValue say what changed
  },
})
```

`computed()` takes the same option as its second argument, and the `onRenderTriggered()` hook does the same for a component that re-renders unexpectedly. All three work in development only.

## Shrink the bug in the playground

When the bug survives all that, make it smaller. The [Vue SFC Playground](https://play.vuejs.org) runs a component in the browser with no build step. Paste the component in, replace the data it fetches with a literal, delete everything that is not involved, and see whether the bug is still there. If it disappears, the cause is in what you deleted: a parent, a store, a global style. Put things back one at a time until it returns. If it stays, you have a twenty-line reproduction, which is exactly what a bug report or a forum question needs.

## Bisect the template with v-if

For "something in this page is wrong and I do not know what", split the problem in half. Wrap the top half of the template in `<template v-if="false">` and reload. If the bug is gone, it was in that half; if it stays, it is in the other. Move the `v-if` and repeat. Four rounds narrow a 200-line template to about twelve lines. `git bisect` does the same with your history: mark a commit that works and one that does not, and it halves the range between them until it names the commit that broke it.

## The checklist in order

1. Read the console. Fix every `[Vue warn]` before chasing the bug; one of them is often the bug.
2. Open Vue DevTools, select the component, and decide whether the state is wrong or the template is.
3. Log `.value`, not the ref, and snapshot reactive objects with `structuredClone(toRaw(state))`.
4. Put a breakpoint or `debugger` on the first line of the handler and step through it. To see what set off a watcher, put it in `onTrigger`.
5. Reproduce it in the playground with literal data.
6. Bisect the template with `v-if`, or the history with `git bisect`.
7. Write the failing test before the fix, so the bug cannot come back quietly.

**Check it stuck**3 questions from the Error handling and debugging quiz.

[**Practice now**](https://vuejs.school/learn/advanced/error-handling-and-debugging?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 7 min read**Vue 3.6 and Vapor Mode: where things stand**](https://vuejs.school/community/blog/vue-3-6-and-vapor-mode-where-things-stand) [Deep dive · 7 min read**Vapor Mode, explained without the hype**](https://vuejs.school/community/blog/vapor-mode-explained-without-the-hype)

```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/a-debugging-checklist-for-vue-apps#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-09-02","description":"Read the warning, open DevTools, set a breakpoint and shrink it in the playground: a fixed order for finding Vue bugs, with the warnings you will meet most.","name":"A debugging checklist for Vue apps","url":"https://vuejs.school/community/blog/a-debugging-checklist-for-vue-apps","breadcrumb":{"@id":"https://vuejs.school/community/blog/a-debugging-checklist-for-vue-apps#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/a-debugging-checklist-for-vue-apps"]}],"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/a-debugging-checklist-for-vue-apps#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-09-02","description":"Read the warning, open DevTools, set a breakpoint and shrink it in the playground: a fixed order for finding Vue bugs, with the warnings you will meet most.","headline":"A debugging checklist for Vue apps","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~QSBkZWJ1Z2dpbmcgY2hlY2tsaXN0IGZvciBWdWUgYXBwcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_A%2Bdebugging%2Bchecklist%2Bfor%2BVue%2Bapps%2Cdescription_~UmVhZCB0aGUgd2FybmluZywgb3BlbiBEZXZUb29scywgc2V0IGEgYnJlYWtwb2ludCBhbmQgc2hyaW5rIGl0IGluIHRoZSBwbGF5Z3JvdW5kOiBhIGZpeGVkIG9yZGVyIGZvciBmaW5kaW5nIFZ1ZSBidWdzLCB3aXRoIHRoZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IG1vc3Qu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hLWRlYnVnZ2luZy1jaGVja2xpc3QtZm9yLXZ1ZS1hcHBzIg%2Cs_rYwWZEaqaHo02mTG.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/a-debugging-checklist-for-vue-apps#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/a-debugging-checklist-for-vue-apps#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/a-debugging-checklist-for-vue-apps#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"A debugging checklist for Vue apps","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_~QSBkZWJ1Z2dpbmcgY2hlY2tsaXN0IGZvciBWdWUgYXBwcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_A%2Bdebugging%2Bchecklist%2Bfor%2BVue%2Bapps%2Cdescription_~UmVhZCB0aGUgd2FybmluZywgb3BlbiBEZXZUb29scywgc2V0IGEgYnJlYWtwb2ludCBhbmQgc2hyaW5rIGl0IGluIHRoZSBwbGF5Z3JvdW5kOiBhIGZpeGVkIG9yZGVyIGZvciBmaW5kaW5nIFZ1ZSBidWdzLCB3aXRoIHRoZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IG1vc3Qu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hLWRlYnVnZ2luZy1jaGVja2xpc3QtZm9yLXZ1ZS1hcHBzIg%2Cs_rYwWZEaqaHo02mTG.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~QSBkZWJ1Z2dpbmcgY2hlY2tsaXN0IGZvciBWdWUgYXBwcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_A%2Bdebugging%2Bchecklist%2Bfor%2BVue%2Bapps%2Cdescription_~UmVhZCB0aGUgd2FybmluZywgb3BlbiBEZXZUb29scywgc2V0IGEgYnJlYWtwb2ludCBhbmQgc2hyaW5rIGl0IGluIHRoZSBwbGF5Z3JvdW5kOiBhIGZpeGVkIG9yZGVyIGZvciBmaW5kaW5nIFZ1ZSBidWdzLCB3aXRoIHRoZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IG1vc3Qu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9hLWRlYnVnZ2luZy1jaGVja2xpc3QtZm9yLXZ1ZS1hcHBzIg%2Cs_rYwWZEaqaHo02mTG.png"}]}
```