The problem: Submitting an Inertia form with invalid data correctly triggers a 422 on the backend, but the frontend's errors object stays empty and no validation messages ever render.
Environment: Laravel 11, Inertia.js 1.x, Vue 3 (also applies to React adapter), PHP 8.3
The Error
POST /projects 422 (Unprocessable Content)
Response body: { "message": "The name field is required.", "errors": { "name": [...] } }
...but in the Vue component:
form.errors.name === undefined
Why It Happens
Inertia has a very specific contract for how validation errors get from a 422 response into your form helper's reactive errors object — and if any link in that chain is broken, the errors exist in the raw HTTP response but never reach your component. The usual causes:
1. Not using Inertia's useForm() helper at all — if the form is submitted via a raw router.post() call or plain Axios instead of useForm().post(), there's no reactive errors object being managed for you; you'd need to manually read the error bag from the response event yourself.
2. Missing the X-Inertia header on the request — this typically happens if a custom Axios interceptor or a manually-configured request bypasses Inertia's own request pipeline, so Laravel's exception handler doesn't know to format the 422 the way Inertia's frontend expects (a redirect-back-with-errors instead of a raw JSON error response).
3. A custom FormRequest overrides failedValidation() and throws a different exception type or returns a plain JSON response manually, bypassing Laravel's default validation exception handling that Inertia's middleware is built to intercept.
The Fix
First, confirm the form actually uses Inertia's useForm() helper, which wires up reactive error handling automatically:
import { useForm } from '@inertiajs/vue3';
const form = useForm({
name: '',
});
function submit() {
form.post('/projects');
}
Reference errors directly from the form object in the template — this reactivity is what breaks if the form wasn't created with useForm():
<input v-model="form.name" />
<div v-if="form.errors.name">{{ form.errors.name }}</div>
If a custom FormRequest overrides failedValidation(), make sure it still throws Laravel's standard exception rather than manually returning JSON:
protected function failedValidation(Validator $validator)
{
- throw new HttpResponseException(response()->json($validator->errors(), 422));
+ throw new ValidationException($validator);
}
Step-by-Step
- Confirm the form is built with
useForm(), not a raw Axios/fetch call or plainrouter.post(). - Check the Network tab request headers for the submission and confirm
X-Inertia: trueis present. - If using a custom
FormRequest, checkfailedValidation()for any manual JSON response and replace it with the standardValidationExceptionthrow. - Confirm the input's error binding in the template reads from
form.errors.fieldNameexactly matching the validation rule's key. - Resubmit the invalid form and confirm
form.errorspopulates in Vue DevTools' component inspector. - Check that error messages render and clear correctly on the next successful submission (Inertia clears
errorsautomatically after a successful response).
Edge case: If your form fields are nested (e.g. an array of line items like items.0.name), the validation rule key and the form.errors lookup key must match exactly including the array index — a rule written as items.*.name in the FormRequest won't automatically map to form.errors['items.0.name'] unless your Blade/Vue error lookup also accounts for the specific index, which is easy to get wrong in dynamically-added form rows.
Still not seeing validation errors after checking all this? Reach out on the Contact page and I'll help debug your form setup.