Error Handling
The server might reject submitted data. Pass errors back to the form and it handles display.
Field-level errors
Pass a { fieldKey: message } object. The error shows on the field, and the form navigates to the step containing it.
<script setup>
import { ref } from 'vue';
const serverErrors = ref({});
async function onSubmit(values) {
try {
await api.submitForm(values);
} catch (err) {
// Server returns: { email: "Already registered" }
serverErrors.value = err.fieldErrors;
}
}
</script>
<template>
<FormRenderer
:definition="definition"
:errors="serverErrors"
@submit="onSubmit"
/>
</template>function MyForm() {
const [errors, setErrors] = useState({});
async function handleSubmit(values) {
try {
await api.submitForm(values);
} catch (err) {
// Server returns: { email: "Already registered" }
setErrors(err.fieldErrors);
}
}
return (
<FormRenderer
definition={definition}
errors={errors}
onSubmit={handleSubmit}
/>
);
}In a multi-step form, if the error is on a field in step 1 and the user is on step 3, the form auto-navigates back to step 1.
Top-level errors
If the error targets a field that doesn't exist (or is hidden), it shows as a banner above the action buttons.
Covers cases like:
- Account suspended
- Rate limit exceeded
- Generic server errors
// Server returns an error for a non-existent field key
setErrors({ _general: 'Your account has been temporarily suspended.' });The banner renders between the fields and the Submit/Continue buttons.
Async step validation errors
See Async Step Validation for how onStepValidate errors display and how to handle network failures.
Loading state
Show a loading indicator on the submit button while the request is pending:
<template>
<FormRenderer
:definition="definition"
:loading="isSubmitting"
@submit="onSubmit"
/>
</template><FormRenderer
definition={definition}
loading={isSubmitting}
onSubmit={handleSubmit}
/>When loading is true, the submit button shows a spinner and is disabled.
Field-level loading
Some fields load data asynchronously (e.g. looking up a city by zip code). Use setFieldLoading on the engine:
<script setup>
import { useFormEngine } from '@formhaus/vue';
const { engine } = useFormEngine(definition);
async function onFieldChange(key, value) {
if (key === 'zipCode' && value.length === 5) {
engine.setFieldLoading('city', true);
const result = await lookupCity(value);
engine.setValue('city', result.name);
engine.setFieldLoading('city', false);
}
}
</script>const engine = useFormEngine(definition);
function onFieldChange(key, value) {
if (key === 'zipCode' && value.length === 5) {
engine.setFieldLoading('city', true);
lookupCity(value).then(result => {
engine.setValue('city', result.name);
engine.setFieldLoading('city', false);
});
}
}Next steps
- Validation: client-side validation rules
- Examples: error handling in real definitions
- Definition Reference: full TypeScript types