Server validation errors arrive after your React form posts to an API, usually as a 4xx response with a list of failed fields. Show them by mapping each field message into the same errors object your inputs already render. A request that never reaches the server is a different failure and deserves a separate message.
Client checks run first for speed, but the server is the only authority, so its errors take priority when they arrive.
Separate network errors from validation errors
Two things can go wrong when a form submits. The request can fail before reaching the server, or the server can reject the data with field errors. Checking response.ok splits these two cases.
async function handleSubmit(e) {
e.preventDefault(); setStatus("sending");
try {
const response = await fetch("/api/signup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(values) });
if (!response.ok) {
const { errors } = await response.json();
setErrors(errors || {});
return;
}
setStatus("done");
} catch {
setStatus("network");
}
}The catch block handles a failed or interrupted request. A non-ok response is the server telling you the data was wrong, so the handler reads the field errors and stores them. On success it clears the errors and marks the form done.
Agree on an error response shape
Pick one shape for validation errors and use it everywhere. A common convention is a top-level errors object keyed by field name, so the client can drop it straight into state. Return a separate key such as form or message for errors that are not tied to one field.
A stable shape means the form never has to guess where a message lives, and the same rendering works for every endpoint.
Map field errors from the response
The server should return an object keyed by field name, like { email: "Already taken" }. Storing it directly in the errors state lets each field render its own message without extra mapping code.
{status === "network" && <p role="alert">Could not reach the server. Try again.</p>}
{errors.email && <p role="alert">{errors.email}</p>}
{errors.form && <p role="alert">{errors.form}</p>}The network message appears only when the request itself failed. Field messages appear under their inputs, and errors.form is the shared slot for a message that is not tied to one field, such as "Email or password is incorrect".
Render the global message above the form with role=alert so assistive technology announces it as soon as it appears.
Match the message to the status
Use the status code to decide how to respond. A 400 or 422 usually carries field errors, a 401 means the user should log in again, and a 500 means the server broke and the user should try later. Keep the form focused on validation: only field errors and form-level messages belong in the errors state, while other failures are handled outside the form.
Do the same with React Hook Form
React Hook Form accepts server errors through setError. Call it once per field in a loop, and use the root key for a form-wide message.
const { register, handleSubmit, setError, formState: { errors } } = useForm();
async function onSubmit(data) {
const response = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const payload = await response.json();
if (!response.ok) {
for (const [field, message] of Object.entries(payload.errors || {})) {
setError(field, { type: "server", message });
}
}
}Each setError call attaches one server message to one field, and the existing errors.field.message rendering shows it. setError also accepts a root key for a global error, such as setError("root.server", { type: "server", message }), which renders through errors.root.server instead of a named field.
Keep the response parsing consistent between plain state and the library so the same backend serves both.
Clear errors when the user retries
Server errors should not stick around after the user fixes the input. Clear the field error when the user edits that field, and clear everything at the start of the next submit. With plain state, reset the errors object in the submit handler before the request, then repopulate it only if the server responds with new failures.
With React Hook Form, call clearErrors before re-submitting, or rely on re-validation to replace old messages.
For the client-side checks that run before the request, see React form validation without a library. For the full library setup, see the React Hook Form tutorial, and for the basic wiring see how to build forms in React.
Rune AI
Key Insights
- Distinguish network errors from validation responses.
- Store server field errors in the same errors state.
- Render each message under its matching field.
- Use a global message for non-field failures.
- With React Hook Form, use setError per field.
Frequently Asked Questions
What is the difference between a network error and a validation error?
How do I map server field errors to my form?
How do I show server errors with React Hook Form?
Conclusion
Treat a server validation response as the same errors object your fields already render. Separate network failures from 4xx validation responses, and set each message on its field.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.