21

Errors

Render field, form-level, and section errors, add server errors, and make error messages accessible.

All errors are issues: an object with a message and an optional path.

{ message: 'Enter a valid email address.', path: ['email'] }
  • path is an array of segments. Segments are property names and array indices: ['contacts', 0, 'email']. Depending on the validator, a segment may be a string, a number, or an object with a key.
  • An issue without a path is a form-level issue.

form.errors is the full list. Each field exposes the issues at its own exact path as field.errors.

Show a field's error

FieldError renders the first message and nothing when there is no error.

<template>
  <Field
    v-slot="{ field }"
    :field="form.field('email')"
  >
    <label :for="field.path">Email</label>

    <input
      v-bind="field.events"
      :id="field.path"
      v-model="field.model"
      type="email"
    >

    <FieldError :errors="field" />
  </Field>
</template>

Style it, change its element, or use a slot. See FieldError.

<template>
  <FieldError
    :errors="field"
    as="p"
    class="text-red-600"
    role="alert"
  />
</template>

Show every error

FieldError shows only the first message. To list all of them, render field.errors.

<template>
  <ul v-if="field.errors.length > 0">
    <li
      v-for="(issue, index) in field.errors"
      :key="index"
    >
      {{ issue.message }}
    </li>
  </ul>
</template>

Show form-level errors

Read form.errors and keep the issues without a path.

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

const formErrors = computed(() => {
  return form.errors.filter((issue) => {
    return !issue.path?.length
  })
})
</script>

<template>
  <p
    v-for="(issue, index) in formErrors"
    :key="index"
    role="alert"
  >
    {{ issue.message }}
  </p>
</template>

You can also pass an issue array straight to FieldError: <FieldError :errors="formErrors" as="p" role="alert" />.

Show errors for a section

A field shows only issues at its exact path. An issue on ['address'] does not appear on the address.city field. To summarize a whole section, filter by the first segment.

function keyOf(segment: PropertyKey | { key: PropertyKey }) {
  return typeof segment === 'object' ? segment.key : segment
}

function sectionErrors(section: string) {
  return form.errors.filter((issue) => {
    return issue.path?.[0] !== undefined && String(keyOf(issue.path[0])) === section
  })
}

Show an error summary

After a failed submit, form.errors holds every problem in the form. It is the natural source for a summary at the top of a long form.

<template>
  <div
    v-if="form.isSubmitted && form.errors.length > 0"
    role="alert"
  >
    <p>Fix {{ form.errors.length }} problem(s) to continue.</p>

    <ul>
      <li
        v-for="(issue, index) in form.errors"
        :key="index"
      >
        {{ issue.message }}
      </li>
    </ul>
  </div>
</template>

Add errors yourself

form.setError(issue) adds an error, typically after a server response.

form.setError({ message: 'This email address is already in use.', path: ['email'] })
form.setError({ message: 'We could not reach the server.' }) // form-level
  • With a path, it replaces the errors already at that exact path.
  • Without a path, it appends a form-level error. Calling it twice adds two.

See Server errors for a complete submit flow.

Start with errors

Pass initialErrors when a form should open with issues, for example after a server-side render.

const form = useForm({
  initialErrors: [{ message: 'Verify this address.', path: ['email'] }],
  initialValues: { email: 'jane@example.com' },
  schema,
})

reset() returns to the current initial errors. reset(values, errors) replaces them.

When errors change

EventEffect on errors
A field validates (blur, change, input, field.validate())Replaces only the errors at that field's exact path.
form.validate()Replaces all errors with the schema's result.
SubmitReplaces all errors with the schema's result. A passing submit clears them before onSubmit runs.
form.setError(issue)Adds or replaces errors at one path, or appends a form-level error.
An array mutationErrors stay attached to the item they belong to. Removed items lose their errors. See Array fields.
form.reset()Restores the initial errors.
Because a field validation replaces the errors at its path, a server error on email disappears the next time email validates and the schema reports no problem there. Form-level server errors stay until the next submit or reset. This is usually what you want, since editing a field makes its server error stale.

Accessible error markup

Connect each message to its control so assistive technology announces it.

<template>
  <Field
    v-slot="{ field }"
    :field="form.field('email')"
  >
    <label :for="field.path">Email</label>

    <input
      :id="field.path"
      v-model="field.model"
      v-bind="field.events"
      type="email"
      :aria-invalid="!field.isValid"
      :aria-describedby="field.isValid ? undefined : `${field.path}-error`"
    >

    <FieldError
      :id="`${field.path}-error`"
      :errors="field"
      role="alert"
    />
  </Field>
</template>

FieldError forwards id, class, role, and other attributes to the element it renders.

Next steps

Copyright © 2026 NotForm
Published under MIT License