React Hook Form 07 ๐ Error Handling โ Reading the errors object and displaying messages
The errors object is the part of the formState that holds the validation failures. It is a nested object that mirrors the shape of the form values: a top-level field’s error is errors.email, a nested field’s error is errors.address.city, and an array field’s error is errors.items[0].name or errors.items.0.name. Each error object has a type property that names the rule that failed, a message property that holds the human-readable text, and a ref property that points to the DOM element. The nested structure is the mechanism that lets the UI read the error for the specific field without parsing a flat list of messages, and the type property is the mechanism that lets the UI distinguish the failure modes when the same field has multiple rules.
The errors object is a part of the formState proxy, which means the component re-renders only when the error properties it reads actually change. A component that reads errors.email does not re-render when errors.password changes, and a component that reads errors.email.message does not re-render when errors.email.type changes without the message changing . The narrow subscription is the mechanism that keeps the error rendering proportional to the actual changes, and it is the reason the error display does not become the performance bottleneck that the controlled-form approach would create.
Key point: The errors object mirrors the form values’ shape. Each error has a type, a message, and a ref. The formState.errors is a proxy, and the component re-renders only when the error properties it reads change. The type is the rule name, and the message is the string the rule returned. The root-level errors are under errors.root, and the field-level errors are under the field’s path.
Why the errors object is the display mechanism
The shape problem. A form value can be a string, an object, an array, or a nested combination of the three. The error for the value must follow the same shape, or the UI would have to flatten the structure to find the error. The errors object mirrors the values’ shape, so the UI reads the error at the same path it reads the value . The errors.address.city is the error for the address.city field, and the errors.items.0.name is the error for the first item’s name. The mirroring is the mechanism that keeps the display code simple.
The type problem. A field can have multiple rulesโthe required and the minLength and the pattern on the email field. The errors.email holds the error for the first failing rule, and the errors.email.type is the rule’s name . The type is the mechanism that lets the UI show the different messages for the different rules, and it is the mechanism that lets the test assert on the specific rule rather than the message string.
The root problem. Some errors do not belong to any fieldโthe network failure, the server error, the form-level validation failure. The errors.root is the object that holds these errors, and the root.server and root.generic are the keys the library uses . The root errors are the mechanism that keeps the form-level errors separate from the field-level errors, and the display is the separate component that reads the errors.root.
The proxy problem. The errors object is a proxy that tracks which properties the component reads. A component that reads errors.email re-renders when the errors.email reference changes, and a component that reads errors.email.message re-renders when the message changes. The proxy is the mechanism that makes the narrow subscription possible, and the narrow subscription is the reason the error display does not re-render the whole form.
The accessibility problem. An error message that is visually displayed but not announced to the screen reader is an error the user with the screen reader does not know about. The role="alert" on the error element, the aria-invalid on the input, and the aria-describedby connecting the input to the error are the mechanisms that make the error accessible . The accessibility is the separate concern from the display, and the error object is the data both concerns read.
a. The errors object structure and the field-level display
The errors object mirrors the form values. The top-level field’s error is errors.fieldName, the nested field’s error is errors.parent.child, and the array field’s error is errors.items[index].field. The error object has the type, the message, and the ref properties.
// The errors object structure
const {
formState: { errors },
} = useForm<LoginFormValues>();
// The top-level field
errors.email?.message; // "The email is required."
errors.email?.type; // "required"
// The nested field
errors.address?.city?.message; // "The city is required."
// The array field
errors.items?.[0]?.name?.message; // "The name is required."
The field-level display is the component that reads the specific error. The component reads the errors.fieldName, and the proxy re-renders it only when that error changes . The narrow read is the mechanism that keeps the re-renders proportional to the changes.
// The field-level display
function EmailField() {
const {
register,
formState: { errors },
} = useForm<LoginFormValues>();
return (
<div>
<input
{...register("email")}
aria-invalid={errors.email ? "true" : "false"}
aria-describedby={errors.email ? "email-error" : undefined}
/>
{errors.email && (
<p id="email-error" role="alert">
{errors.email.message}
</p>
)}
</div>
);
}
The aria-invalid is the attribute that tells the screen reader the field is invalid, and the aria-describedby is the attribute that connects the input to the error element. The role="alert" is the attribute that makes the screen reader announce the error when it appears . The three attributes are the accessibility mechanism, and the errors.email is the data they read.
The ErrorMessage component from the @hookform/error-message package is the alternative. The component accepts the errors object, the name, and the render function, and it renders the message when the error exists.
// The ErrorMessage component
import { ErrorMessage } from "@hookform/error-message";
function EmailField() {
const { register, formState: { errors } } = useForm();
return (
<div>
<input {...register("email")} />
<ErrorMessage
errors={errors}
name="email"
render={({ message }) => <p role="alert">{message}</p>}
/>
</div>
);
}
The ErrorMessage is the convenience, and the manual errors.email && <p> is the equivalent. The package is the optional dependency, and the manual rendering is the mechanism that keeps the dependency count low.
b. The root errors and the multiple-rule display
The root errors are the errors that do not belong to any field. The setError("root.server", { ... }) is the mechanism that stores the server error under the root, and the errors.root?.server is the path the UI reads.
// The root error display
function FormError() {
const {
formState: { errors },
} = useForm();
return (
<>
{errors.root?.server && (
<p role="alert">{errors.root.server.message}</p>
)}
{errors.root?.generic && (
<p role="alert">{errors.root.generic.message}</p>
)}
</>
);
}
The root.server is the server error, and the root.generic is the general error. The two keys are the library’s convention, and the errors.root is the object that holds them . The root display is the separate component, and the separation is the mechanism that keeps the form-level errors out of the field-level components.
The multiple-rule display is the component that reads the type to show the different messages for the different rules. The errors.email.type is the rule name, and the switch on the type is the mechanism that maps the rule to the message.
// The multiple-rule display
function EmailField() {
const { register, formState: { errors } } = useForm();
const getMessage = () => {
if (!errors.email) return null;
switch (errors.email.type) {
case "required":
return "The email is required.";
case "pattern":
return "The email format is invalid.";
default:
return "The email is invalid.";
}
};
return (
<div>
<input {...register("email")} />
{errors.email && <p role="alert">{getMessage()}</p>}
</div>
);
}
The switch on the type is the mechanism that distinguishes the rules. The message is the string the rule returned, and the type is the rule’s name. The two are the data the display reads, and the display is the component that maps them to the user-facing text.
The criteriaMode option is the mechanism that changes the error shape for the multiple rules on the same field. The default is "firstError", and the "all" is the mode that collects every failing rule’s error into the errors.email.types object.
// The criteriaMode: "all"
const { register, formState: { errors } } = useForm({
criteriaMode: "all",
});
// The errors.email.types is the object of the failed rules.
errors.email?.types?.required; // "The email is required."
errors.email?.types?.pattern; // "The email format is invalid."
The criteriaMode: "all" is the mechanism that shows every rule’s message, and the types object is the collection. The default "firstError" is the mechanism that shows only the first failing rule, and the choice is the UX decision.
Complete Example Session
// ============================================
// PART 1: THE BASIC ERROR DISPLAY
// ============================================
import { useForm } from "react-hook-form";
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
defaultValues: { email: "", password: "" },
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))} noValidate>
<input {...register("email", { required: "The email is required." })} />
{errors.email && <p role="alert">{errors.email.message}</p>}
<input {...register("password", { required: "The password is required." })} type="password" />
{errors.password && <p role="alert">{errors.password.message}</p>}
</form>
);
}
// ============================================
// PART 2: THE NESTED ERROR
// ============================================
<input {...register("address.city", { required: "The city is required." })} />
{errors.address?.city && <p role="alert">{errors.address.city.message}</p>}
// The nested error mirrors the nested field.
// ============================================
// PART 3: THE ARRAY ERROR
// ============================================
<input {...register("items.0.name", { required: "The name is required." })} />
{errors.items?.[0]?.name && <p role="alert">{errors.items[0].name.message}</p>}
// The array error uses the index path.
// ============================================
// PART 4: THE ERROR TYPE
// ============================================
<input
{...register("email", {
required: "The email is required.",
pattern: { value: /^\S+@\S+$/i, message: "The email is invalid." },
})}
/>
{errors.email && <p role="alert">{errors.email.type}: {errors.email.message}</p>}
// The type is the failed rule name.
// ============================================
// PART 5: THE ACCESSIBILITY ATTRIBUTES
// ============================================
<input
{...register("email")}
aria-invalid={errors.email ? "true" : "false"}
aria-describedby={errors.email ? "email-error" : undefined}
/>
{errors.email && <p id="email-error" role="alert">{errors.email.message}</p>}
// The aria attributes connect the input to the error.
// ============================================
// PART 6: THE ROOT ERROR
// ============================================
{errors.root?.server && <p role="alert">{errors.root.server.message}</p>}
// The root error is the form-level error.
// ============================================
// PART 7: THE MULTIPLE RULES
// ============================================
const { register, formState: { errors } } = useForm({
criteriaMode: "all",
});
// The errors.email.types holds every failed rule.
// ============================================
// PART 8: THE ERRORMESSAGE COMPONENT
// ============================================
import { ErrorMessage } from "@hookform/error-message";
<ErrorMessage
errors={errors}
name="email"
render={({ message }) => <p role="alert">{message}</p>}
/>
// The component is the optional convenience.
// ============================================
// PART 9: THE NARROW SUBSCRIPTION
// ============================================
function ErrorSummary() {
const { formState: { errors } } = useForm();
// The component reads only errors.
// It re-renders only when errors changes.
return <>{Object.keys(errors).length > 0 && <p>There are errors.</p>}</>;
}
// The narrow read keeps the re-renders proportional.
// ============================================
// PART 10: THE CHECKLIST
// ============================================
// [ ] The errors object mirrors the form values.
// [ ] The error has the type and the message.
// [ ] The root errors are under errors.root.
// [ ] The aria attributes connect the input to the error.
// [ ] The criteriaMode: "all" collects every rule's error.
// [ ] The ErrorMessage component is the optional convenience.
The ten parts covered the basic error display, the nested error, the array error, the error type, the accessibility attributes, the root error, the multiple rules, the ErrorMessage component, the narrow subscription, and the checklist. The through-line is that the errors object mirrors the values, and the display reads the specific path.
Quick Reference
The error object
| Property | Type | Purpose |
|---|---|---|
type | string | The failed rule name |
message | string | The error message |
ref | HTMLElement | The DOM element |
types | object | The multiple rules (with criteriaMode: "all") |
The error paths
| Field type | Path |
|---|---|
| The top-level | errors.email |
| The nested | errors.address.city |
| The array | errors.items[0].name |
| The root | errors.root.server |
The accessibility attributes
| Attribute | Purpose |
|---|---|
role="alert" | Announce the error to the screen reader |
aria-invalid | Mark the input as invalid |
aria-describedby | Connect the input to the error element |
The criteriaMode
| Mode | Behavior |
|---|---|
"firstError" | The first failing rule’s error |
"all" | Every failing rule’s error in the types object |
Best Practices
โ Do This:
// Read the specific error path
{errors.email && <p role="alert">{errors.email.message}</p>} // โ
// Use the aria attributes
aria-invalid={errors.email ? "true" : "false"} // โ
// Use the criteriaMode: "all" for the multiple messages
useForm({ criteriaMode: "all" }) // โ
โ Don’t Do This:
// Read the whole formState in the error display
const { formState } = useForm(); // the wider re-render // โ
// Assume the nested error is a flat key
errors["address.city"] // the wrong path // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The error does not render | The wrong path is used | Check the nested path |
| The screen reader does not announce | The role="alert" is missing | Add the attribute |
| The multiple rules show one message | The criteriaMode is not "all" | Set the mode |
| The form re-renders on every keystroke | The errors is read in the wrong scope | Read the specific path |
Real-World Examples
1. The basic error
{errors.email && <p role="alert">{errors.email.message}</p>}
2. The nested error
{errors.address?.city && <p>{errors.address.city.message}</p>}
3. The array error
{errors.items?.[0]?.name && <p>{errors.items[0].name.message}</p>}
4. The error type
{errors.email?.type === "pattern" && <p>The format is invalid.</p>}
5. The root error
{errors.root?.server && <p role="alert">{errors.root.server.message}</p>}
6. The aria attributes
<input aria-invalid={errors.email ? "true" : "false"} />
7. The criteriaMode
useForm({ criteriaMode: "all" })
8. The types object
errors.email?.types?.required
9. The ErrorMessage component
<ErrorMessage errors={errors} name="email" render={({ message }) => <p>{message}</p>} />
10. The narrow subscription
const { formState: { errors } } = useForm();
Visual
The Error Object Structure
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE errors OBJECT MIRRORS THE VALUES โ
โ โ
โ The values The errors โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ email: "" โ โ email: { โ โ
โ โ address: { โ โ type, โ โ
โ โ city: "" โ โ message โ โ
โ โ } โ โ } โ โ
โ โ โ โ address: { โ โ
โ โ โ โ city: {...} โ โ
โ โ โ โ } โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ The UI reads the same path it reads the value. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Field Display
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE FIELD DISPLAY โ
โ โ
โ The component reads errors.email โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The proxy re-rendersโ Only when errors.email changes โ
โ โ the component โ โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The error message โ The aria attributes โ
โ โ is rendered โ connect the input โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Root Error
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE ROOT ERROR โ
โ โ
โ setError("root.server", { message: "Network error" }) โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ errors.root.server โ The form-level error โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The separate โ The error is rendered outside โ
โ โ component reads it โ the field components โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| The error object | type, message, ref, types |
| The top-level path | errors.fieldName |
| The nested path | errors.parent.child |
| The array path | errors.items[index].field |
| The root path | errors.root.key |
| The accessibility | role="alert", aria-invalid, aria-describedby |
| The criteriaMode | "firstError" or "all" |
Key takeaways:
- The errors object mirrors the form values’ shape. The top-level field’s error is
errors.email, the nested field’s error iserrors.address.city, and the array field’s error iserrors.items[0].name. The mirroring is the mechanism that lets the UI read the error at the same path it reads the value . - Each error has a
typeand amessage. Thetypeis the failed rule name, and themessageis the string the rule returned. The two are the data the display reads, and the switch on the type is the mechanism that maps the rule to the message . - The root errors are under
errors.root. Theroot.serverand theroot.genericare the keys for the server and the general errors, and the separate component is the mechanism that keeps the form-level errors out of the field-level display . - The
criteriaMode: "all"collects every failed rule’s error. Theerrors.email.typesis the object of the failed rules, and the default"firstError"shows only the first failing rule’s message . - The accessibility attributes are the mechanism that announces the errors. The
role="alert", thearia-invalid, and thearia-describedbyare the attributes that connect the input to the error, and the screen reader is the mechanism that reads them .
Remember: The React Hook Form error handling is the errors object that mirrors the values, and the display that reads the specific path. The type is the rule name, the message is the error text, and the root is the form-level errors. The criteriaMode: "all" is the multiple rules, and the accessibility attributes are the announcement. The narrow read is the mechanism that keeps the re-renders proportional.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!