Server-side errors
Client validation only covers what the browser can see. Uniqueness, authorization and rate limits come back from the server after everything local has already passed.
Return an object from your submit callback and its keys are merged into errors:
async function onSubmit(form: HTMLFormElement) {
try {
await api.signup(new FormData(form));
} catch (error) {
return { email: 'That address is already registered' };
}
}
Any key that matches a registered field also gets aria-invalid="true" on its element. Keys that match nothing are still written to the store, which is how you render a form-level message:
<span>{errors.form}</span>
Success and failure
Returning undefined is the success path. It clears every error and sets isSubmitted to true. Returning an object is the failure path, and isSubmitted stays false.
Do not return an empty object to mean success. {} is an object, so it takes the failure path, clears nothing and sets nothing.
// Success
if (response.ok) return;
// Failure
return { email: 'Already registered' };
Throwing
An exception thrown inside your callback propagates. isSubmitting is left true, so catch inside the callback and return an error object instead of letting it escape:
async function onSubmit(form: HTMLFormElement) {
try {
await api.signup(new FormData(form));
} catch (error) {
return { form: 'Something went wrong. Try again.' };
}
}
Clearing server errors
Server errors behave like any other error once they are in the store. The next input into a field clears its message. A form-level key that belongs to no field is not attached to any input, so it stays until the next successful submit or a form reset.