---
title: "Vue warnings decoded: the ten you will meet first · vuejs.school"
description: "Ten Vue 3.5 console warnings you will meet early, with the exact text, what causes each one and the few lines of code that make it go away."
canonical_url: "https://vuejs.school/community/blog/vue-warnings-decoded"
meta:
  "og:description": "Ten Vue 3.5 console warnings you will meet early, with the exact text, what causes each one and the few lines of code that make it go away."
  "og:image": "https://vuejs.school/_og/d/a_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdA%2Cdescription_~VGVuIFZ1ZSAzLjUgY29uc29sZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IGVhcmx5LCB3aXRoIHRoZSBleGFjdCB0ZXh0LCB3aGF0IGNhdXNlcyBlYWNoIG9uZSBhbmQgdGhlIGZldyBsaW5lcyBvZiBjb2RlIHRoYXQgbWFrZSBpdCBnbyBhd2F5Lg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWUtd2FybmluZ3MtZGVjb2RlZCI%2Cs_WHuf0xRr1uCSwAXj.png"
  "og:image:alt": "Vue warnings decoded: the ten you will meet first · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Vue warnings decoded: the ten you will meet first · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdA%2Cdescription_~VGVuIFZ1ZSAzLjUgY29uc29sZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IGVhcmx5LCB3aXRoIHRoZSBleGFjdCB0ZXh0LCB3aGF0IGNhdXNlcyBlYWNoIG9uZSBhbmQgdGhlIGZldyBsaW5lcyBvZiBjb2RlIHRoYXQgbWFrZSBpdCBnbyBhd2F5Lg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWUtd2FybmluZ3MtZGVjb2RlZCI%2Cs_WHuf0xRr1uCSwAXj.png"
  "twitter:image:alt": "Vue warnings decoded: the ten you will meet first · vuejs.school"
---

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

## How to read a warning

A warning has two parts: the message, then a component trace. Here is a real one:

```
[Vue warn]: Missing required prop: "count"
  at <Counter>
  at <App>
```

The first `at` line is the component where the problem was found. The lines below it are its parents, up to the root. Open the file named on the first line, unless the message is about something passed in, such as a prop or an attribute. Then the mistake is usually in the parent one line down, where the component is used.

A few of these checks also exist as type errors. If your project runs `vue-tsc`, the type checker for `.vue` files, it reports a wrong prop type, a missing prop, a write to a prop and an undeclared event before the browser ever sees them.

## Props: invalid type, missing, or readonly

Take a small quantity picker. It receives the current quantity through `v-model` and a maximum through a prop.

*components/QuantityPicker.vue*

```
<script setup lang="ts">
defineProps<{ max: number }>()
const quantity = defineModel<number>({ required: true })
</script>

<template>
  <button type="button" :disabled="quantity >= max" @click="quantity++">
    Add one ({{ quantity }})
  </button>
</template>
```

Now use it carelessly: `<QuantityPicker max="10" />`. You get two warnings.

```
[Vue warn]: Invalid prop: type check failed for prop "max". Expected Number with value 10, got String with value "10".
[Vue warn]: Missing required prop: "modelValue"
```

The first happens because `max="10"` is a plain HTML attribute, and attributes are always strings. To pass the number 10, bind it with a colon: `:max="10"`. The colon means "evaluate this as JavaScript".

The second warning names `modelValue` because that is the prop `defineModel` creates behind the scenes. The fix is to pass it: `<QuantityPicker v-model="qty" :max="10" />`. If a prop really is optional, make it optional in the type and give it a default, for example `const { max = 10 } = defineProps<{ max?: number }>()`.

The third props warning appears when a component changes a prop it was given. Here is the picker written without `defineModel`, with `quantity` as an ordinary prop:

*components/QuantityPicker.vue**Breaks***

```
<script setup lang="ts">
const props = defineProps<{ quantity: number, max: number }>()

function add() {
  props.quantity++
}
</script>
```

```
[Vue warn] Set operation on key "quantity" failed: target is readonly.
```

Props flow down from the parent, and the parent owns them. The fix is the `defineModel` version above: the child writes to `quantity`, and Vue sends an `update:modelValue` event so the parent changes its own value.

This warning looks different from the others. There is no colon after `[Vue warn]` and no component trace, because the check comes from Vue's reactivity system rather than from a component. To find the culprit, search your code for the key it names. In a component written with the Options API, the same mistake reads `Attempting to mutate prop "quantity". Props are readonly.`

## Extraneous attributes and undeclared events

An attribute you pass to a component that is not a declared prop, such as `type` or `placeholder`, normally lands on the component's root element. That breaks when there is more than one root:

*components/FieldRow.vue**Breaks***

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

defineProps<{ label: string }>()
const id = useId()
</script>

<template>
  <label :for="id">{{ label }}</label>
  <input :id="id">
</template>
```

Used as `<FieldRow label="Email" type="email" placeholder="you@example.com" />`, it prints:

```
[Vue warn]: Extraneous non-props attributes (type, placeholder) were passed to component but could not be automatically inherited because component renders fragment or text or teleport root nodes.
```

A fragment is a template with several top-level elements. Vue cannot guess which one should get the attributes, so you tell it:

*components/FieldRow.vue**Works***

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

defineOptions({ inheritAttrs: false })
defineProps<{ label: string }>()
const id = useId()
</script>

<template>
  <label :for="id">{{ label }}</label>
  <input :id="id" v-bind="$attrs">
</template>
```

`inheritAttrs: false` stops the automatic behaviour, and `v-bind="$attrs"` puts every extra attribute on the input. A sibling warning, `Extraneous non-emits event listeners`, means the same thing for an `@click` or other listener.

Events have their own warning: `Component emitted event "remove" but it is neither declared in the emits option nor as an "onRemove" prop.` Vue only checks this when the component declares at least one event, so the usual cause is a new `$emit('remove', id)` in the template that never made it into `defineEmits`. Add it to the list: `defineEmits<{ toggle: [id: number], remove: [id: number] }>()`. Declared events are also left out of `$attrs`, so an `@click` that your component emits itself is not also attached to the root element, where it would fire twice.

## Unresolved components and missing templates

`Failed to resolve component: BaseCard` means the template uses a tag that Vue does not know. The message continues with `If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` Check these causes in order:

1. In `<script setup>`, the import is missing or its name differs from the tag. `import Card from './BaseCard.vue'` makes `<Card>` available, not `<BaseCard>`.
2. The tag comes from a plugin that was never installed. `Failed to resolve component: RouterLink` almost always means `app.use(router)` is missing, or a test mounted the component without the router.
3. It really is a custom element, such as a web component. Then tell the compiler to leave it alone:

*vite.config.ts*

```
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: { isCustomElement: tag => tag.startsWith('my-') },
      },
    }),
  ],
})
```

With this setting, Vue leaves every tag that starts with `my-` to the browser. Use the prefix your custom elements share.

`Component is missing template or render function:` means Vue found the component but has nothing to draw. The usual cause is a `.vue` file with a script but no `<template>` block: a new file that so far has only its `<script setup>`, or a typo like `<templte>`, which Vue treats as an unknown custom block. Open the component named in the first `at` line and check its template tag. A completely empty `.vue` file never gets this far: Vue's compiler for `.vue` files rejects it with `At least one <template> or <script> is required in a single file component.`

## Injection not found

`injection "user" not found.` means a component called `inject('user')` and no ancestor provided that key. A provider has to be above the component in the tree; a sibling or a child does not count. Typical causes are a typo in a string key, or a test that mounts the component on its own.

Use a typed symbol key so a typo becomes a type error, and decide what happens when nothing is provided:

*composables/user.ts*

```
import { inject, type InjectionKey, type Ref } from 'vue'

export interface User { name: string }
export const UserKey: InjectionKey<Ref<User | null>> = Symbol('user')

export function useUser() {
  const user = inject(UserKey)
  if (!user) throw new Error('useUser() needs a <UserProvider> above it')
  return user
}
```

Vue still prints its warning, now as `injection "Symbol(user)" not found.`, and your error follows it with the name of the missing provider. That is faster to act on than an `undefined` three files later. In a Vue Test Utils test, pass the value with `global: { provide: { [UserKey]: ref(null) } }`.

## Hydration mismatch between server and browser

Server-side rendering, as in Nuxt, renders the HTML on the server and then hydrates it in the browser: Vue walks the existing HTML and attaches itself instead of drawing it again. If the browser computes different output, you get:

```
[Vue warn]: Hydration text content mismatch on <p>Server time 10:00</p>
  - rendered on server: Server time 10:00
  - expected on client: Server time 10:01
```

Vue then logs `Hydration completed but contains mismatches.` The cause is a value that differs between the two runs: the current time, `Math.random()`, `window.innerWidth`, or something read from `localStorage`. Render a neutral value on both sides and fill in the browser-only value after mounting:

*components/LoadedAt.vue*

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

const loadedAt = ref<string | null>(null)

onMounted(() => {
  loadedAt.value = new Date().toLocaleTimeString()
})
</script>

<template>
  <p>Loaded {{ loadedAt ? `at ${loadedAt}` : 'just now' }}</p>
</template>
```

`onMounted` only runs in the browser, after hydration, so both runs render "Loaded just now" first. Other cases have their own fixes:

- For element ids that must match, use `useId()`.
- In Nuxt, wrap a browser-only widget in `<ClientOnly>`.
- When a difference is expected and harmless, Vue 3.5 accepts `data-allow-mismatch="text"` on the element to silence that one check.

## Maximum recursive updates exceeded

This one is an error, not a warning, and it often shows up as `Uncaught (in promise)`:

```
Maximum recursive updates exceeded in component <Counter>. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.
```

Vue stopped an endless loop: something changes the state it depends on, which runs it again, which changes the state again. Vue gives up after 100 rounds. The common causes are:

- a function called from the template that also changes a ref
- an `onUpdated` hook that changes state
- a watcher that changes the value it watches

The fix is to keep reads and writes apart. Templates and `computed` only read. Changes happen in event handlers, or in a watcher that writes to a different ref than the one it watches.

**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/vue-warnings-decoded#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-08-25","description":"Ten Vue 3.5 console warnings you will meet early, with the exact text, what causes each one and the few lines of code that make it go away.","name":"Vue warnings decoded: the ten you will meet first","url":"https://vuejs.school/community/blog/vue-warnings-decoded","breadcrumb":{"@id":"https://vuejs.school/community/blog/vue-warnings-decoded#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/vue-warnings-decoded"]}],"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/vue-warnings-decoded#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-08-25","description":"Ten Vue 3.5 console warnings you will meet early, with the exact text, what causes each one and the few lines of code that make it go away.","headline":"Vue warnings decoded: the ten you will meet first","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdA%2Cdescription_~VGVuIFZ1ZSAzLjUgY29uc29sZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IGVhcmx5LCB3aXRoIHRoZSBleGFjdCB0ZXh0LCB3aGF0IGNhdXNlcyBlYWNoIG9uZSBhbmQgdGhlIGZldyBsaW5lcyBvZiBjb2RlIHRoYXQgbWFrZSBpdCBnbyBhd2F5Lg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWUtd2FybmluZ3MtZGVjb2RlZCI%2Cs_WHuf0xRr1uCSwAXj.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/vue-warnings-decoded#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/vue-warnings-decoded#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/vue-warnings-decoded#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Vue warnings decoded: the ten you will meet first","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_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdA%2Cdescription_~VGVuIFZ1ZSAzLjUgY29uc29sZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IGVhcmx5LCB3aXRoIHRoZSBleGFjdCB0ZXh0LCB3aGF0IGNhdXNlcyBlYWNoIG9uZSBhbmQgdGhlIGZldyBsaW5lcyBvZiBjb2RlIHRoYXQgbWFrZSBpdCBnbyBhd2F5Lg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWUtd2FybmluZ3MtZGVjb2RlZCI%2Cs_WHuf0xRr1uCSwAXj.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlIHdhcm5pbmdzIGRlY29kZWQ6IHRoZSB0ZW4geW91IHdpbGwgbWVldCBmaXJzdA%2Cdescription_~VGVuIFZ1ZSAzLjUgY29uc29sZSB3YXJuaW5ncyB5b3Ugd2lsbCBtZWV0IGVhcmx5LCB3aXRoIHRoZSBleGFjdCB0ZXh0LCB3aGF0IGNhdXNlcyBlYWNoIG9uZSBhbmQgdGhlIGZldyBsaW5lcyBvZiBjb2RlIHRoYXQgbWFrZSBpdCBnbyBhd2F5Lg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWUtd2FybmluZ3MtZGVjb2RlZCI%2Cs_WHuf0xRr1uCSwAXj.png"}]}
```