You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When an action throws data({ errors }, { status: 422 }), React Router treats it as a route error, renders the nearest errorElement, and skips the normal route component. The form UI never mounts, so hooks like useActionData/useErrors cannot render inline field errors. Teams are encouraged to “throw to fail fast,” but doing so currently forces a full error boundary experience for routine validation.
Throwing is also the ergonomic way to keep validation helper code clean—without it, every action must manually handle validation at the top level (the only place you can return), which makes shared helpers awkward and noisy.
Apps commonly have a small helper hook (e.g., our useErrors) that reads useActionData and normalizes field errors for inputs. Thrown 422 responses bypass that hook entirely because the route never renders, so teams must fall back to manual, non-throwing validation just to keep inline UX working.
Goals
Keep the ergonomic, explicit “throw on validation failure” flow.
Let the route continue rendering so forms can show inline errors.
Avoid per-route boilerplate error elements that re-render the same form.
Graceful 4xx handling mode (opt-in)
When explicitly opted in, treat marked thrown Responses as action data. A marker could be an init flag/header (e.g., X-Remix-Validation: 1) or a helper like validationError(). Deliver the body to useActionData while keeping the route tree mounted. Unmarked 4xx (e.g., a real 404) and all 5xx/non-Response errors stay on the existing error-boundary path, preserving current semantics.
Inline validation accessor
Provide a tiny helper (e.g., useInlineValidation() or an addition to useRouteError) that returns marked 4xx validation errors when the route still renders. Keeps scope small—no new error model—just an ergonomic way to read the same { errors } shape whether it came from action data or a marked throw.
Per-route toggle
Add a route option like handleValidationInPlace: true (or inlineErrors: true) that opts a route into the behavior from option 1. Default stays as-is for backward compatibility.
Standard validation throw helper
Provide a helper throw validationError(errors, init?) that marks the Response (e.g., header X-Remix-Validation: 1). The router recognizes it and routes it to useActionData instead of errorElement, keeping the UI.
Error-element passthrough
Allow errorElement to opt into rendering the normal route element tree while receiving the error (flag like renderChildrenOnError: true or a useRenderRouteOnError() hook). This avoids duplicating layout just to show validation errors.
Middleware-based handling
Since middleware is now stable, allow a middleware to intercept thrown Responses and, when marked for validation, convert them into action data (or attach a flag the router respects) before the error boundary runs. This keeps the core router simpler while giving apps a first-class, documented escape hatch.
Compatibility notes
Default behavior should remain unchanged to avoid breaking existing error boundaries.
Opt-in flags/hooks should keep 5xx and unexpected errors on the current error path.
Should work for loader/action throws (and ideally coexist with deferred/streaming).
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
The problem
When an action throws
data({ errors }, { status: 422 }), React Router treats it as a route error, renders the nearesterrorElement, and skips the normal route component. The form UI never mounts, so hooks likeuseActionData/useErrorscannot render inline field errors. Teams are encouraged to “throw to fail fast,” but doing so currently forces a full error boundary experience for routine validation.Throwing is also the ergonomic way to keep validation helper code clean—without it, every action must manually handle validation at the top level (the only place you can
return), which makes shared helpers awkward and noisy.Apps commonly have a small helper hook (e.g., our
useErrors) that readsuseActionDataand normalizes field errors for inputs. Thrown 422 responses bypass that hook entirely because the route never renders, so teams must fall back to manual, non-throwing validation just to keep inline UX working.Goals
Options
Graceful 4xx handling mode (opt-in)
When explicitly opted in, treat marked thrown Responses as action data. A marker could be an init flag/header (e.g.,
X-Remix-Validation: 1) or a helper likevalidationError(). Deliver the body touseActionDatawhile keeping the route tree mounted. Unmarked 4xx (e.g., a real 404) and all 5xx/non-Response errors stay on the existing error-boundary path, preserving current semantics.Inline validation accessor
Provide a tiny helper (e.g.,
useInlineValidation()or an addition touseRouteError) that returns marked 4xx validation errors when the route still renders. Keeps scope small—no new error model—just an ergonomic way to read the same{ errors }shape whether it came from action data or a marked throw.Per-route toggle
Add a route option like
handleValidationInPlace: true(orinlineErrors: true) that opts a route into the behavior from option 1. Default stays as-is for backward compatibility.Standard validation throw helper
Provide a helper
throw validationError(errors, init?)that marks the Response (e.g., headerX-Remix-Validation: 1). The router recognizes it and routes it touseActionDatainstead oferrorElement, keeping the UI.Error-element passthrough
Allow
errorElementto opt into rendering the normal route element tree while receiving the error (flag likerenderChildrenOnError: trueor auseRenderRouteOnError()hook). This avoids duplicating layout just to show validation errors.Middleware-based handling
Since middleware is now stable, allow a middleware to intercept thrown Responses and, when marked for validation, convert them into action data (or attach a flag the router respects) before the error boundary runs. This keeps the core router simpler while giving apps a first-class, documented escape hatch.
Compatibility notes
Beta Was this translation helpful? Give feedback.
All reactions