How to Show Server Validation Errors in React Forms

Display server validation errors in React forms by mapping field messages from the response and showing a global error for non-field failures.

6 min read

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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
{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.

App.jsxApp.jsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between a network error and a validation error?

A network error means the request never reached a working server. A validation error means the server responded, usually with a 4xx status, and listed which fields failed.

How do I map server field errors to my form?

Return field errors as an object keyed by field name, such as { email: "Already taken" }. Store that object in your errors state and render each message under its field.

How do I show server errors with React Hook Form?

Call setError for each field with a type and message. For a form-wide error, use the root key, such as setError("root.server", { type: "server", message }).

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.