Server Errors
Client-side validation can catch many problems before a request is sent, but a server can still reject otherwise valid form data.
For example, the backend may know that:
- an email address is already registered
- a username is already taken
- a value conflicts with another record
- a business rule failed
- a permission or authentication check failed
NotForm represents these errors using the same StandardSchemaV1.Issue structure used by client-side validation.
This means server errors can live in the same form.errors collection and work with the same field-level error APIs.
The Error Model
NotForm errors are an array:
StandardSchemaV1.Issue[]
A field error can look like:
{
path: ['email'],
message: 'This email is already registered.',
}
Nested fields use path segments:
{
path: ['profile', 'email'],
message: 'This email is already registered.',
}
The path is what connects the error to NotField, NotMessage, errorsMap, and getFieldErrors().
Applying Server Errors
Use setError() when applying one issue:
form.setError({
message: 'This email is already registered.',
path: ['email'],
})
Use setErrors() when the server returns multiple issues:
form.setErrors([
{
message: 'This email is already registered.',
path: ['email'],
},
{
message: 'Username is already taken.',
path: ['username'],
},
])
These errors immediately become part of:
form.errors
and:
form.errorsMap
Basic Submission Example
A typical submission flow looks like this:
const form = useNotForm({
schema,
async onSubmit(values) {
const response = await fetch('/api/users', {
body: JSON.stringify(values),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
if (!response.ok) {
const data = await response.json()
form.setErrors(data.errors)
return
}
console.log('User created')
},
})
Client-side validation happens before onSubmit is called.
If the schema validation succeeds, the request is sent.
If the server rejects the request, its issues can then be added to the form.
setError
Use setError() for an individual issue:
form.setError({
message: 'This email is already registered.',
path: ['email'],
})
If an active error already exists for the same normalized path, it is replaced.
This is useful when handling one specific backend error:
if (response.status === 409) {
form.setError({
message: 'This email is already registered.',
path: ['email'],
})
}
setErrors
Use setErrors() when the backend returns a collection of issues:
form.setErrors([
{
message: 'This email is already registered.',
path: ['email'],
},
{
message: 'Username is already taken.',
path: ['username'],
},
])
setErrors() replaces the current form.errors collection.
This is useful when the server's response represents the complete validation state for the failed request.
Server Response Mapping
Not every API returns StandardSchemaV1.Issue[] directly.
For example, your backend might return:
{
"errors": {
"email": "This email is already registered.",
"username": "Username is already taken."
}
}
That response needs to be transformed into NotForm's issue structure:
const errors = Object.entries(data.errors).map(([path, message]) => ({
message,
path: path.split('.'),
}),)
form.setErrors(errors)
For:
{
"email": "This email is already registered.",
"user.name": "Name is required"
}
the resulting issues are:
[
{
message: 'This email is already registered.',
path: ['email'],
},
{
message: 'Name is required',
path: ['user', 'name'],
},
]
The important part is that the server error paths correspond to the paths used by the form.
Nested Field Errors
Nested fields use the same path structure as the rest of NotForm.
For example:
form.setErrors([
{
message: 'This email is already registered.',
path: ['user', 'email'],
},
{
message: 'Name is required.',
path: ['user', 'name'],
},
])
The errors can then be accessed using the same paths:
form.getFieldErrors('user.email')
form.errorsMap['user.email']
And displayed through:
<NotMessage path="user.email" />
<NotMessage path="user.name" />
Displaying Server Errors
Server errors use the same field components as schema errors.
<NotField
path="email"
v-slot="{ events }"
>
<input
v-model="form.values.email"
v-bind="events"
/>
<NotMessage path="email" />
</NotField>
There is no separate server-error component.
The field simply displays whatever issues currently exist for its path.
Multiple Errors for One Field
A field can have multiple issues.
Use getFieldErrors() when you need all of them:
const errors = form.getFieldErrors('password')
for (const error of errors) {
console.log(error.message)
}
Or use the field's errors slot prop:
<NotField
path="password"
v-slot="{ errors, events }"
>
<input
v-model="form.values.password"
v-bind="events"
/>
<ul>
<li
v-for="(error, index) in errors"
:key="index"
>
{{ error.message }}
</li>
</ul>
</NotField>
NotMessage intentionally displays only the first active issue for a field.
Global Server Errors
Some failures do not belong to a particular field.
Examples include:
Rate limit exceeded
Session expired
Permission denied
Payment failed
Unexpected server error
NotForm's issue model is path-oriented, so application-level errors that are not tied to a field should generally be handled separately from field errors.
For example:
const form = useNotForm({
schema,
async onSubmit(values) {
const response = await saveUser(values)
if (!response.ok) {
const data = await response.json()
if (data.fieldErrors) {
form.setErrors(data.fieldErrors)
}
if (data.globalError) {
showGlobalError(data.globalError)
}
}
},
})
This keeps field validation inside NotForm while allowing application-level failures to be rendered elsewhere.
Client and Server Validation Together
A normal submission can involve both kinds of validation:
User input
↓
Field validation
↓
Form submission
↓
Complete schema validation
↓
onSubmit()
↓
Server validation
↓
Server issues
↓
form.setErrors()
↓
Field errors displayed
The same error collection is used throughout the form.
This means the UI does not need to know whether an error came from Zod, Valibot, another Standard Schema validator, or your backend.
Server Errors and Form State
Server errors participate in the normal form state:
form.errors
form.errorsMap
form.isValid
form.getFieldErrors('email')
For example:
form.setError({
message: 'Email is already registered.',
path: ['email'],
})
console.log(form.errorsMap.email)
console.log(form.isValid.value)
Because isValid is derived from the current error collection, adding a server error makes the form invalid until that error is replaced or cleared.
Clearing Errors
Clear All Errors
Use:
form.clearErrors()
This removes every active error.
Replace All Errors
Calling:
form.setErrors([])
also results in an empty error collection.
The important distinction is that setErrors() replaces the collection with the supplied issues, while clearErrors() is the explicit API for removing all errors.
Updating a Server Error
Use setError() when the server returns a replacement issue for the same field:
form.setError({
message: 'The email is unavailable.',
path: ['email'],
})
The existing issue for that path is replaced instead of accumulating another copy.
Server Errors After Resubmission
A common flow is:
const form = useNotForm({
schema,
async onSubmit(values) {
const response = await saveUser(values)
if (!response.ok) {
form.setErrors(await response.json())
return
}
form.reset()
},
})
On a successful submission, resetting the form can establish a fresh baseline.
On a failed submission, keep the current values and apply the backend issues.
Demo
Recommended Backend Contract
A particularly convenient backend contract is an array of path-aware issues:
{
"issues": [
{
"path": ["email"],
"message": "This email is already registered."
},
{
"path": [
"profile",
"name"
],
"message": "Name is required."
}
]
}
This maps naturally to:
form.setErrors(data.issues)
If your backend uses another format, map it at the API boundary rather than teaching your form components about the server's response shape.