Error messages
Render validation messages consistently, including multiple errors per field.
Validation errors live in formState.errors, keyed by the same path you registered. Each error has a type (the rule that failed, such as required) and a message. This page covers where messages come from, how to render them, and how to show every failed rule at once.
Write the message in the rule
Every built-in rule accepts a message. Pass a string instead of true, or an object with value and message:
register("username", {required: "Enter a username.",minLength: { value: 3, message: "Use at least 3 characters." },})
A validate function returns true when the value is valid, or the message to show. Use an object of named functions when a field has several custom checks, so each one gets its own error type:
register("password", {validate: {length: (value) => value.length >= 8 || "Use at least 8 characters.",number: (value) => /\d/.test(value) || "Include a number.",},})
With a schema resolver, messages come from the schema instead, for example z.string().min(3, "Use at least 3 characters.").
Render messages
Read a message with optional chaining. Nested fields follow the registered path, so address.city is at errors.address?.city:
{errors.address?.city && <p role="alert">{errors.address.city.message}</p>}
The ErrorMessage component does the lookup for you. It accepts any registered path as a string, renders nothing when the field is valid, and reads errors from control or the nearest FormProvider.
Show every failed rule
By default, validation stops at the first failed rule for each field. Set criteriaMode: "all" to collect every failure. They appear in errors.<field>.types, keyed by rule, and ErrorMessage passes them to render as messages. This is useful for password rules that the user fixes one by one.
The example below combines all three: rule messages, every failed password rule, and a nested field.
import { ErrorMessage, useForm } from "react-hook-form"type FormValues = {username: stringpassword: stringaddress: { city: string }}export default function SignupForm() {const {register,control,handleSubmit,formState: { errors },} = useForm<FormValues>({criteriaMode: "all",defaultValues: { username: "", password: "", address: { city: "" } },})return (<form noValidate onSubmit={handleSubmit(console.log)}><label htmlFor="username">Username</label><inputid="username"aria-invalid={!!errors.username}aria-describedby="username-error"{...register("username", {required: "Enter a username.",minLength: { value: 3, message: "Use at least 3 characters." },})}/><ErrorMessagecontrol={control}name="username"render={({ message }) => (<p id="username-error" role="alert">{message}</p>)}/><label htmlFor="password">Password</label><inputid="password"type="password"aria-invalid={!!errors.password}aria-describedby="password-errors"{...register("password", {required: "Enter a password.",validate: {length: (value) =>value.length >= 8 || "Use at least 8 characters.",number: (value) => /\d/.test(value) || "Include a number.",},})}/><ErrorMessagecontrol={control}name="password"render={({ messages }) =>messages && (<ul id="password-errors" role="alert">{Object.entries(messages).map(([type, message]) => (<li key={type}>{message}</li>))}</ul>)}/><label htmlFor="city">City</label><inputid="city"aria-invalid={!!errors.address?.city}{...register("address.city", { required: "Enter a city." })}/>{errors.address?.city && (<p role="alert">{errors.address.city.message}</p>)}<button type="submit">Sign up</button></form>)}
A reusable message component
Destructuring formState.errors in the form component re-renders the whole form whenever any error changes. In a large form, render each message in a small component that subscribes only to its own field with useFormState:
import {get,useFormState,type FieldError,type FieldPath,type FieldValues,} from "react-hook-form"export function FieldMessage<T extends FieldValues>({name,}: {name: FieldPath<T>}) {// Subscribes to this field's errors only, so only this message re-renders.const { errors } = useFormState<T>({ name })const error: FieldError | undefined = get(errors, name)if (!error?.message) return nullreturn (<p id={`${name}-error`} role="alert">{error.message}</p>)}
Use it inside a FormProvider, for example <FieldMessage<FormValues> name="address.city" />. Point the input's aria-describedby at the same id so screen readers read the message with the field. The get helper exported by react-hook-form resolves dotted paths such as address.city, which plain property access cannot.
Errors that don't belong to one field
- Server and request errors: set them with
setError("root.server", ...)and read them fromerrors.root?.server. See Handling server errors. - Field array rules: a
ruleserror onuseFieldArrayappears aterrors.<arrayName>?.root.
Accessibility checklist
- Set
aria-invalidon the input when it has an error. - Link the message with
aria-describedby, using anidthat is unique on the page. - Render the message in an element with
role="alert"so it is announced when it appears. - Keep messages specific and actionable: "Use at least 8 characters" rather than "Invalid".
See Accessibility for focus management and testing with a screen reader.