Async Step Validation
Run a server check before advancing to the next step. Pass onStepValidate, get errors back or null to proceed.
When to use it
- Email/username uniqueness ("already registered")
- Promo code or invite code validation
- Address verification
- Any server-side business rule that blocks the next step
Setup
import { FormRenderer } from '@formhaus/react';
async function validateStep(stepId: string, values: Record<string, unknown>) {
if (stepId === 'account') {
const res = await fetch('/api/check-email', {
method: 'POST',
body: JSON.stringify({ email: values.email }),
});
const data = await res.json();
if (data.taken) {
return { email: 'This email is already registered' };
}
}
return null;
}
function SignupForm() {
return (
<FormRenderer
definition={definition}
onStepValidate={validateStep}
onSubmit={handleSubmit}
/>
);
}<script setup>
import { FormRenderer } from '@formhaus/vue';
async function validateStep(stepId, values) {
if (stepId === 'account') {
const res = await fetch('/api/check-email', {
method: 'POST',
body: JSON.stringify({ email: values.email }),
});
const data = await res.json();
if (data.taken) {
return { email: 'This email is already registered' };
}
}
return null;
}
</script>
<template>
<FormRenderer
:definition="definition"
:on-step-validate="validateStep"
@submit="onSubmit"
/>
</template>Error handling
Field errors
Return an object with field keys. Errors show on the fields, same as client-side validation.
return { email: 'Already registered', username: 'Contains banned word' };Errors for hidden or missing fields
If you return an error for a field that doesn't exist or is currently hidden, it shows as a banner above the buttons (same as top-level errors).
return { _rateLimit: 'Too many attempts. Try again in 5 minutes.' };Network errors
If your function throws, stepValidating resets and the error propagates to your code.
// React example: catch and show a toast
async function handleNext() {
try {
await engine.next();
} catch (err) {
toast.error('Connection failed. Check your internet and try again.');
}
}With FormRenderer or HeadlessFormRenderer, rejections reach the onError callback, or display as a form-level error when it is omitted. With onError, the form-level error is not shown. You can also return a field or top-level error from onStepValidate:
async function validateStep(stepId, values) {
try {
const res = await fetch('/api/validate-step', { ... });
return await res.json();
} catch {
return { _network: 'Could not reach the server. Try again.' };
}
}Edge cases
Back while validating. Renderers disable Back while an async action is pending. If low-level core navigation changes the step during a pending guard, its result is discarded.
Last step. The renderer Submit action does not invoke onStepValidate. Perform final server validation in the submit handler before saving.
next() returns false on the final step without running validation or navigation hooks. Use onBeforeSubmit for cancellable checks before submission; see lifecycle hooks.
Type reference
type StepValidateFn = (
stepId: string,
values: Record<string, unknown>,
) => Promise<Record<string, string> | null | void>;The callback receives:
stepId- theidof the step being validated (from your definition)values- visible field values across the active path, not just the current step. In routed forms, retained answers from skipped branches are excluded.
Return:
{ key: message }- errors to show. Keys match field keys in your definition.nullorundefined- no errors, advance to next step.
Using the engine directly
If you use FormEngine without the React/Vue adapter, await next():
import { FormEngine } from '@formhaus/core';
const engine = new FormEngine(definition, initialValues, {
validators: { ... },
onStepValidate: async (stepId, values) => {
// your server call
},
});
// In your navigation handler:
const advanced = await engine.next();
// Check loading state for your UI:
engine.stepValidating; // true while navigation validation and hooks are runningnext() also awaits the before/after navigation hooks; a before-hook can cancel an otherwise valid transition.
Next steps
- Validation: client-side validation rules (runs before async)
- Error Handling: how errors display, loading states
- Multi-Step Forms: step navigation, conditional steps, progress
- Definition Reference: all TypeScript types