Server errors
Submit to an API and show field-level and form-level errors that the server returns.
Client-side validation cannot know everything. A server may reject an email that is already registered, or fail for reasons unrelated to any field. This recipe turns those responses into errors your form displays.
The flow
- Submit. The schema validates first, so
onSubmitonly runs with valid data. - Send the data. Inspect the response.
- For field problems, call
form.setError({ message, path }). - For everything else, call
form.setError({ message }), which has nopathand is a form-level error.
Example
<script setup lang="ts">
import { Field, FieldError, Form, useForm } from 'notform'
import { computed } from 'vue'
import { z } from 'zod'
const form = useForm({
initialValues: { email: '', username: '' },
async onSubmit(data) {
try {
const response = await fetch('/api/register', {
body: JSON.stringify(data),
headers: { 'content-type': 'application/json' },
method: 'POST',
})
if (response.status === 422) {
// { errors: { email: 'Already in use.', username: '...' } }
const body = await response.json() as { errors: Record<string, string> }
for (const [name, message] of Object.entries(body.errors)) {
form.setError({ message, path: [name] })
}
return
}
if (!response.ok) {
form.setError({ message: 'Something went wrong. Please try again.' })
return
}
form.reset()
} catch {
form.setError({ message: 'We could not reach the server. Check your connection.' })
}
},
schema: z.object({
email: z.email('Enter a valid email address.'),
username: z.string().min(3, 'Use at least 3 characters.'),
}),
})
const formErrors = computed(() => {
return form.errors.filter((issue) => {
return !issue.path?.length
})
})
</script>
<template>
<Form
:form="form"
novalidate
>
<FieldError
:errors="formErrors"
as="p"
role="alert"
/>
<Field
v-slot="{ field }"
:field="form.field('username')"
>
<label :for="field.path">Username</label>
<input
:id="field.path"
v-model="field.model"
v-bind="field.events"
>
<FieldError :errors="field" />
</Field>
<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"
>
<FieldError :errors="field" />
</Field>
<button
type="submit"
:disabled="form.isSubmitting"
>
Create account
</button>
</Form>
</template>
How long server errors last
| Error | Cleared when |
|---|---|
A field error from setError({ path }) | That field validates again and the schema reports no issue there, for example on blur or when the user edits it. Also on reset(), and on the next submit. |
A form-level error from setError({ message }) | The next submit, a form.validate(), or reset(). |
Editing a field with a server error therefore removes the error as soon as the schema passes, which is usually right: the server's complaint was about the old value.
To keep a server error visible while the user edits, store it yourself and display it next to FieldError.
Nested and array paths
A path is an array of segments. Use indices for array items.
form.setError({ message: 'Email already invited.', path: ['contacts', 1, 'email'] })
form.setError({ message: 'Invalid city.', path: ['address', 'city'] })
If the server returns dot paths, split them: path: key.split('.'). Numeric strings such as '1' match array indices.
Notes
setErrorwith a path replaces errors at that path. Without a path it appends, so avoid calling it repeatedly for the same message.- Errors thrown (not returned) from
onSubmitare not caught by NotForm. Wrap risky calls intry/catch, as above. See Submission and reset. isSubmittingstaystrueuntilonSubmitsettles, so the button stays disabled during the request.