Zod errors from server action aren't propagated to field error.
Describe the bug
Hi :)
I'm trying to implement a tanstack form in my project, but I've encountered a problem.
Namely, validation errors from the server-side action using the ZOD aren't being sent to the form's inputs. They're visible in state and the form itself, but they're not visible in field.state.meta.errors 🙆♀️
I followed the official guide and some YouTube tutorials, but there aren't enough examples online for this scenario :/
I'd like to avoid returning my own object from "zodErrors" and parsing it in the client component; I'd like to use the returned function from createServerValidate.
I validate all the code in onBlur and onSubmit using the ZOD scheme => and everything works. ✔️
However, when I do the same in the server action:
const serverValidate = createServerValidate({
onServerValidate: formSchema,
});
the errors don't get sent to the fields. They're visible in state and form, but they don't propagate downstream.
Normal behavior
Validation with ZOD, from onBlur / onChange:
Behavior when errors are comming from server action
Your minimal, reproducible example
https://codesandbox.io/p/github/AdamZajler/tanstack-form-nextjs-mui-server-action-zod/main // https://github.com/AdamZajler/tanstack-form-nextjs-mui-server-action-zod
Steps to reproduce
- Run project
- Enter some values, for ex. firstName:
123and random valid e-mail address - Press enter
- Now you will be seing a VALID response, all errors will be visible under inputs
- Now fill form with firstName with required length, ex.
123456 - Now errors will not be shown under inputs
Expected behavior
Errors from server-side field validation will appear under specific fields in the client component; just like with onBlur/onChange validation, because the error format is the same
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
- Ubuntu 24
- Chrome
TanStack Form adapter
react-form
TanStack Form version
v1.19.2
TypeScript version
v5.9.2
Additional context
Has anyone encountered this problem? How did the list solve it? Can anyone provide a working example?
Any update on this?
!!! Also the createServerValidate is using the old getHeader completely breaking this. For now you can return your errorMap.onServer in onMount() => { fields: state.errorMap.onServer }
🐛 Confirming the bug with a minimal React Router example
I ran into the same issue — server-side field errors (from createServerValidate) appear correctly in form.state.errorMap.onServer.fields, but they never propagate to individual field.state.meta.errors. This means UI conditions like:
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
never become true, and the inputs don’t show server errors, even though they exist in the form state.
Here’s the shape I get on the client after submitting:
{
"errorMap": {
"onServer": {
"fields": {
"username": "That username is already taken."
}
}
},
"values": {
"username": "test"
},
"errors": []
}
So the data makes it back to the client, but it’s not merged into meta.errors for that specific field.
✅ Expected behavior
Server-side field errors returned from createServerValidate (via ServerValidateError) should be merged into each field.state.meta.errors, just like onBlur / onChange validation errors are.
💡 Minimal Reproduction (React Router)
Click to view code
import { z } from 'zod';
import {
formOptions,
initialFormState,
createServerValidate,
ServerValidateError,
} from '@tanstack/react-form/remix';
import { mergeForm, useForm, useTransform } from '@tanstack/react-form';
import { Form } from 'react-router';
import type { Route } from './+types/profile-form-example';
// Schema
const schema = z.object({
username: z.string().min(3, 'Min 3 chars'),
});
// Client form config
const formConfig = formOptions({
defaultValues: { username: '' },
validators: { onSubmit: schema },
});
// Server validation that always fails username
const serverValidate = createServerValidate({
...formConfig,
onServerValidate: ({ value }) => {
if (value.username) {
return { fields: { username: 'That username is already taken.' } };
}
},
});
// Action
export async function action({ request }: Route.ActionArgs) {
try {
const formData = await request.formData();
const validated = await serverValidate(formData);
return validated;
} catch (err) {
if (err instanceof ServerValidateError) {
console.log(
'server errorMap.onServer.fields:',
err.formState.errorMap.onServer?.fields,
);
return err.formState;
}
throw err;
}
}
// Component
export default function ProfileFormExample({ actionData }: Route.ComponentProps) {
const form = useForm({
...formConfig,
transform: useTransform(
base => mergeForm(base, actionData ?? initialFormState),
[actionData],
),
});
return (
<main style={{ padding: 16 }}>
<Form method="post" onSubmit={() => form.handleSubmit()}>
<form.Field
name="username"
children={field => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid;
return (
<div style={{ display: 'grid', gap: 8, maxWidth: 360 }}>
<label htmlFor={field.name}>Username</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
aria-invalid={isInvalid || undefined}
/>
{isInvalid && field.state.meta.errors.length > 0 && (
<p style={{ color: 'crimson' }}>
{field.state.meta.errors.join(', ')}
</p>
)}
<button type="submit">Submit</button>
<pre style={{ fontSize: 12, background: '#f6f6f6', padding: 8 }}>
{JSON.stringify(
{
'field.meta.isTouched': field.state.meta.isTouched,
'field.meta.isValid': field.state.meta.isValid,
'field.meta.errors': field.state.meta.errors, // stays empty
'form.errorMap.onServer.fields':
form.state.errorMap.onServer?.fields ?? null, // contains { username: '...' }
},
null,
2,
)}
</pre>
</div>
);
}}
/>
</Form>
</main>
);
}
🧩 Observed behavior
- Client-side Zod validation (
onSubmit) works correctly. - The
ServerValidateErroris caught and returned from the action. - The client receives
errorMap.onServer.fields.username. - The field’s
meta.errorsremain empty, so UI doesn’t update.
🔍 Summary
It seems mergeForm (or the internal handling of onServer error maps) doesn’t populate meta.errors for each field when the new state comes from the server. Everything else merges correctly.
This makes server-side validation errors invisible at the field level, even though they’re present in the form state.
Encountering this problem now but it seems even more generic than just Zod errors. Any errors that you send aren't linked to a field at all, even if you try your best to say that they are invalid.
!!! Also the createServerValidate is using the old getHeader completely breaking this. For now you can return your errorMap.onServer in onMount() => { fields: state.errorMap.onServer }
For anyone landing on this issue, I encountered the same getHeader issue (@elmarvr) when using the latest @tanstack/react-start. #1771 should fix it!
Also I think it would be nice to throw server error and n different circumstances. Not just the initial parse.
solved by using this in remix/react-router7
useEffect(() => {
if (fetcher.data) {
form.setErrorMap(fetcher.data.errorMap)
}
}, [fetcher.data])
you can probably replace fetcher with your actionData in Next.
the transform: useTransform(...mergeForm ...) function seems useless. The useTransform is not event exported from @tanstack/react-form, I had to import it from @tanstack/react-form-remix. Which docs and examples do not mention. Docs for useTransform are missing. This lib and its docs are a joke. Sorry.