Skip to main content

Error Handling Guide

⚠️
Error Handling Approach

El Form handles errors differently from React Hook Form:

  • No "root" errors: Use setError("general", message) instead of setError("root", message)
  • Field-specific errors: All errors are managed through setError(fieldName, message) and displayed via formState.errors
  • Consistent API: All el-form packages use the same error handling approach

For general errors: Simply use setError("general", message) and display with formState.errors.general

El Form provides comprehensive error handling capabilities with built-in validation, manual error setting, and customizable error displays. This guide covers all aspects of error management in your forms.

Default Error Handling​

By default, El Form displays validation errors automatically when form fields are touched and validation fails:

import { AutoForm } from "el-form-react-components";
import { z } from "zod";

const userSchema = z
.object({
firstName: z.string().min(1, "First name is required"),
email: z.string().email("Please enter a valid email"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"Password must contain uppercase, lowercase, and number"
),
confirmPassword: z.string(),
age: z.number().min(18, "Must be at least 18 years old"),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});

function UserForm() {
return (
<AutoForm
schema={userSchema}
onSubmit={(data) => console.log("Success:", data)}
onError={(errors) => console.log("Validation errors:", errors)}
/>
);
}

The form automatically:

  • Shows field-specific errors when touched
  • Prevents submission when invalid
  • Calls onError with validation errors
  • Provides accessible error messages

Manual Error Management​

El Form provides powerful methods to set and clear errors manually, perfect for custom validation scenarios and API error handling.

Setting Errors Manually​

Use the setError method to set field-specific errors:

import { useForm } from "el-form-react-hooks";

function RegistrationForm() {
const { register, handleSubmit, setError, clearErrors, formState } = useForm({
defaultValues: { email: "", username: "" },
});

const checkEmailAvailability = async () => {
const email = formState.values.email;

if (!email) return;

try {
const response = await fetch(`/api/check-email/${email}`);
const data = await response.json();

if (data.exists) {
setError("email", "This email is already registered");
} else {
clearErrors("email");
console.log("Email is available!");
}
} catch (error) {
setError("email", "Unable to verify email. Please try again.");
}
};

const handleSubmit = async (data) => {
try {
await registerUser(data);
console.log("Registration successful!");
} catch (apiError) {
// Handle API validation errors
if (apiError.fieldErrors) {
Object.entries(apiError.fieldErrors).forEach(([field, message]) => {
setError(field, message);
});
} else {
setError("general", "Registration failed. Please try again.");
}
}
};

return (
<form onSubmit={handleSubmit(handleSubmit)}>
<div>
<input {...register("email")} placeholder="Email" />
<button type="button" onClick={checkEmailAvailability}>
Check Availability
</button>
{formState.errors.email && (
<p className="error">{formState.errors.email}</p>
)}
</div>

<div>
<input {...register("username")} placeholder="Username" />
{formState.errors.username && (
<p className="error">{formState.errors.username}</p>
)}
</div>

{/* General form errors */}
{formState.errors.general && (
<div className="general-error">{formState.errors.general}</div>
)}

<button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? "Registering..." : "Register"}
</button>
</form>
);
}

Error Management Methods​

MethodDescriptionExample
setError(field, message)Set error on specific fieldsetError("email", "Email taken")
clearErrors(field?)Clear specific or all errorsclearErrors("email") or clearErrors()
trigger(field?)Manually trigger validationtrigger("email") or trigger()

Common Error Scenarios​

API Validation Errors​

Handle server-side validation errors from your API:

const handleSubmit = async (data) => {
try {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});

if (!response.ok) {
const errorData = await response.json();

if (errorData.fieldErrors) {
// Handle field-specific errors
Object.entries(errorData.fieldErrors).forEach(([field, message]) => {
setError(field, message);
});
} else {
// Handle general errors
setError("general", errorData.message || "Something went wrong");
}
return;
}

console.log("Success!");
} catch (error) {
setError("general", "Network error. Please try again.");
}
};

Conditional Validation​

Set errors based on complex business logic:

const validateBusinessRules = (values) => {
clearErrors(); // Clear previous errors

// Age restriction for certain roles
if (values.role === "admin" && values.age < 21) {
setError("age", "Admin users must be at least 21 years old");
}

// Email domain restrictions
if (values.email?.endsWith("@competitor.com")) {
setError("email", "Company email addresses not allowed");
}

// Cross-field validation
if (values.startDate && values.endDate) {
if (new Date(values.startDate) >= new Date(values.endDate)) {
setError("endDate", "End date must be after start date");
}
}
};

// Use with watch to trigger on changes
const values = watch();
useEffect(() => {
validateBusinessRules(values);
}, [values]);

Custom Error Components​

Customize how errors are displayed by providing custom error components to AutoForm:

Elegant Error Style​

import React from "react";

const ElegantErrorComponent = ({ errors, touched }) => {
const errorEntries = Object.entries(errors).filter(
([field]) => touched[field]
);

if (errorEntries.length === 0) return null;

return (
<div className="p-4 bg-gradient-to-r from-pink-50 to-rose-50 border-2 border-pink-200 rounded-xl mb-4 shadow-sm">
<div className="flex items-center mb-3">
<div className="w-6 h-6 bg-pink-500 rounded-full flex items-center justify-center mr-3">
<span className="text-white text-sm font-bold">!</span>
</div>
<h3 className="text-lg font-semibold text-pink-800">
Oops! Let's fix these issues:
</h3>
</div>
<ul className="space-y-2 pl-9">
{errorEntries.map(([field, error]) => (
<li key={field} className="text-pink-700 flex items-start">
<span className="font-medium capitalize mr-2">{field}:</span>
<span>{error}</span>
</li>
))}
</ul>
</div>
);
};

<AutoForm
schema={userSchema}
customErrorComponent={ElegantErrorComponent}
onSubmit={handleSubmit}
/>;

Minimal Error Style​

const MinimalErrorComponent = ({ errors, touched }) => {
const errorEntries = Object.entries(errors).filter(
([field]) => touched[field]
);

if (errorEntries.length === 0) return null;

return (
<div className="border-l-4 border-orange-400 bg-orange-50 p-3 mb-4">
<div className="text-sm text-orange-700">
{errorEntries.map(([field, error]) => (
<div key={field} className="mb-1 last:mb-0">
<span className="font-medium capitalize">{field}:</span> {error}
</div>
))}
</div>
</div>
);
};

Toast-Style Errors​

const ToastErrorComponent = ({ errors, touched }) => {
const errorEntries = Object.entries(errors).filter(
([field]) => touched[field]
);

if (errorEntries.length === 0) return null;

return (
<div className="fixed top-4 right-4 max-w-sm z-50">
<div className="bg-red-500 text-white p-4 rounded-lg shadow-lg">
<div className="flex items-start">
<div className="flex-shrink-0">
<span className="text-lg">⚠️</span>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium">Validation Errors</h3>
<div className="mt-1 text-sm opacity-90">
{errorEntries.length} field{errorEntries.length > 1 ? "s" : ""}{" "}
need attention
</div>
<ul className="mt-2 text-xs space-y-1">
{errorEntries.map(([field, error]) => (
<li key={field}>• {error}</li>
))}
</ul>
</div>
</div>
</div>
</div>
);
};

Async Validation Error Handling​

Handle errors from asynchronous validation:

function EmailValidationForm() {
const { register, formState } = useForm({
fieldValidators: {
email: {
onChangeAsync: async ({ value }) => {
if (!value) return { isValid: true };

try {
const response = await fetch(`/api/validate-email?email=${value}`);
const data = await response.json();

if (data.exists) {
return {
errors: { email: "This email is already registered" },
isValid: false,
};
}

return { isValid: true };
} catch (error) {
return {
errors: { email: "Unable to validate email. Please try again." },
isValid: false,
};
}
},
asyncDebounceMs: 500,
},
},
});

return (
<form>
<div>
<input {...register("email")} placeholder="Email" />
{formState.errors.email && (
<span className="text-red-500">{formState.errors.email}</span>
)}
</div>
</form>
);
}

Error State Management​

Error Persistence​

Errors persist until explicitly cleared or overridden:

const { setError, clearErrors, formState } = useForm();

// Set an error
setError("email", "This email is invalid");

// Error persists until cleared
console.log(formState.errors.email); // "This email is invalid"

// Clear the error
clearErrors("email");
console.log(formState.errors.email); // undefined

// Or override with new error
setError("email", "Email is required");

Error Priority​

When multiple validation sources set errors for the same field:

  1. Manual errors (via setError) take highest priority
  2. Field validators override schema validation
  3. Schema validation is the base level
const form = useForm({
validators: { onChange: schema }, // Lower priority
fieldValidators: {
email: {
onChange: customValidator, // Medium priority
},
},
});

// Highest priority
form.setError("email", "Manual error message");

Touched State​

Errors are typically only shown for "touched" fields:

const { formState } = useForm();

// Check if field has been touched
if (formState.touched.email && formState.errors.email) {
// Show error for touched field
return <span className="error">{formState.errors.email}</span>;
}

// Or show all errors regardless of touched state
if (formState.errors.email) {
return <span className="error">{formState.errors.email}</span>;
}

Error Accessibility​

El Form automatically provides accessibility features for errors:

Screen Reader Support​

// AutoForm automatically generates:
<input
id="email"
aria-invalid="true"
aria-describedby="email-error"
// other props...
/>
<div id="email-error" className="error">
Invalid email address
</div>

Custom Accessible Errors​

When building custom components, maintain accessibility:

function AccessibleField({ name, label, error, ...props }) {
const errorId = `${name}-error`;

return (
<div>
<label htmlFor={name}>{label}</label>
<input
id={name}
{...props}
aria-invalid={!!error}
aria-describedby={error ? errorId : undefined}
/>
{error && (
<div id={errorId} role="alert" className="error">
{error}
</div>
)}
</div>
);
}

Testing Error Handling​

Unit Testing​

Test error scenarios in your forms:

import { render, fireEvent, waitFor } from "@testing-library/react";
import { useForm } from "el-form-react-hooks";

test("displays validation errors", async () => {
function TestForm() {
const { register, handleSubmit, formState } = useForm({
validators: {
onChange: z.object({
email: z.string().email(),
}),
},
});

return (
<form onSubmit={handleSubmit(() => {})}>
<input {...register("email")} data-testid="email" />
{formState.errors.email && (
<span data-testid="email-error">{formState.errors.email}</span>
)}
</form>
);
}

const { getByTestId, queryByTestId } = render(<TestForm />);

// Type invalid email
fireEvent.change(getByTestId("email"), { target: { value: "invalid" } });

// Wait for validation
await waitFor(() => {
expect(queryByTestId("email-error")).toBeInTheDocument();
});
});

Integration Testing​

Test API error integration:

test("handles API errors", async () => {
// Mock API to return validation errors
fetch.mockResolvedValueOnce({
ok: false,
json: () =>
Promise.resolve({
fieldErrors: { email: "Email already exists" },
}),
});

const { getByTestId } = render(<RegistrationForm />);

fireEvent.change(getByTestId("email"), {
target: { value: "test@example.com" },
});
fireEvent.click(getByTestId("submit"));

await waitFor(() => {
expect(getByTestId("email-error")).toHaveTextContent(
"Email already exists"
);
});
});

Best Practices​

1. Provide Clear Error Messages​

// Good: Specific, actionable messages
z.string().email("Please enter a valid email address");
z.string().min(8, "Password must be at least 8 characters");

// Bad: Generic messages
z.string().email("Invalid");
z.string().min(8, "Too short");

2. Handle Network Errors Gracefully​

const handleSubmit = async (data) => {
try {
await submitForm(data);
} catch (error) {
if (error.name === "NetworkError") {
setError(
"general",
"Connection failed. Please check your internet and try again."
);
} else if (error.fieldErrors) {
Object.entries(error.fieldErrors).forEach(([field, message]) => {
setError(field, message);
});
} else {
setError("general", "Something went wrong. Please try again.");
}
}
};

3. Clear Errors Appropriately​

// Clear errors when user starts fixing them
useEffect(() => {
if (formState.values.email) {
clearErrors("email");
}
}, [formState.values.email]);

// Or clear all errors when form is reset
const handleReset = () => {
reset();
clearErrors();
};
// Group related validation into sections
const validateAddressSection = (values) => {
if (values.requiresShipping) {
if (!values.address) setError("address", "Address required for shipping");
if (!values.city) setError("city", "City required for shipping");
if (!values.zipCode) setError("zipCode", "ZIP code required for shipping");
}
};

Error Component Examples​

The library includes 6 different error component styles to demonstrate customization possibilities:

  1. Default - Clean professional styling
  2. Elegant - Pink gradient with rounded design
  3. Minimal - Orange border-left, compact
  4. Dark Mode - Dark theme with red accents
  5. Playful - Colorful gradient with emojis
  6. Toast - Fixed position notifications

You can use these as inspiration or starting points for your own custom error components.

Next Steps​