Submitting & Error Handling

@submit.prevent — Run Async, Catch Errors

Submitting & Error Handling

The submit flow follows a small, consistent pattern. Prevent default, run async work, show errors, and reset or navigate on success.

4 min read Level 2/5 #vue#forms
What you'll learn
  • Use @submit.prevent on the form
  • Disable controls while submitting
  • Surface server errors per field

Every form ends with submit. The pattern is small enough to memorize: prevent the default, run async work, handle errors per field, and clean up on success.

The Pattern

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

const form = reactive({ email: '', password: '' })
const errors = reactive<Record<string, string>>({})
const submitting = ref(false)
const submitError = ref<string | null>(null)

async function onSubmit() {
  submitting.value = true
  submitError.value = null
  for (const key of Object.keys(errors)) delete errors[key]

  try {
    await api.signIn(form)
    // success — navigate or reset
  } catch (err: any) {
    if (err.status === 400 && err.fieldErrors) {
      Object.assign(errors, err.fieldErrors)
    } else {
      submitError.value = err.message ?? 'Something went wrong'
    }
  } finally {
    submitting.value = false
  }
}
</script>

<template>
  <form @submit.prevent="onSubmit">
    <input v-model="form.email" :aria-invalid="!!errors.email" />
    <p v-if="errors.email">{{ errors.email }}</p>

    <input v-model="form.password" type="password" :aria-invalid="!!errors.password" />
    <p v-if="errors.password">{{ errors.password }}</p>

    <p v-if="submitError" class="form-error">{{ submitError }}</p>

    <button :disabled="submitting">
      {{ submitting ? 'Signing in…' : 'Sign in' }}
    </button>
  </form>
</template>

Why @submit.prevent

Using a form element gives you native goodies: Enter submits, screen readers announce it, the browser remembers values. @submit.prevent keeps all of that while letting you handle submission in JS.

Server-Driven Field Errors

When the server returns 4xx validation errors (a { fieldErrors: { email: '...' } } payload), merge them into the same errors object your client validation uses. The UI does not care where errors come from.

After Success

Three common follow-ups, pick whichever fits your flow:

  • Reset the form: Object.assign(form, { email: '', password: '' })
  • Navigate: router.push('/dashboard')
  • Show a toast and stay put

That wraps up the forms section — next we look at routing with vue-router.

Vue Router — Introduction →