We use cookies to enhance your experience on the site
CodeWorlds

Error Handling in Forms

Imagine your space station has a crew registration panel. Each form field is like a sensor on the ship -- if the data is incorrect, the system must immediately notify the operator before the launch sequence is initiated. Error handling in forms is a key UX element of every React application.

Form Validation Patterns

In React, we can distinguish two main approaches to form validation:

1. Client-side Validation

Validation performed in the browser before data is sent to the server. It's fast and provides immediate feedback.

1function RegistrationForm() {
2  const [email, setEmail] = useState('');
3  const [errors, setErrors] = useState({});
4
5  const validateEmail = (value) => {
6    if (!value) return 'Email is required';
7    if (!value.includes('@')) return 'Invalid email format';
8    return null;
9  };
10
11  const handleChange = (e) => {
12    const value = e.target.value;
13    setEmail(value);
14    const error = validateEmail(value);
15    setErrors(prev => ({ ...prev, email: error }));
16  };
17
18  return (
19    <div>
20      <input value={email} onChange={handleChange} />
21      {errors.email && <p className="error">{errors.email}</p>}
22    </div>
23  );
24}

2. Server-side Validation

The server validates the data after the form is submitted and returns errors. It's essential for security -- client-side validation can be bypassed.

1const handleSubmit = async (formData) => {
2  try {
3    const response = await fetch('/api/register', {
4      method: 'POST',
5      body: JSON.stringify(formData),
6    });
7    const result = await response.json();
8
9    if (!response.ok) {
10      // Server returned validation errors
11      setErrors(result.errors);
12    }
13  } catch (error) {
14    setErrors({ general: 'Connection error with server' });
15  }
16};

Displaying Field-Level Errors

Each form field should have its own error message, displayed directly below the field. It's like individual alarms for each system on a spacecraft.

1function CrewRegistration() {
2  const [formData, setFormData] = useState({
3    name: '', callsign: '', rank: ''
4  });
5  const [errors, setErrors] = useState({});
6  const [touched, setTouched] = useState({});
7
8  const validate = (field, value) => {
9    switch (field) {
10      case 'name':
11        if (!value.trim()) return 'Name is required';
12        if (value.length < 2) return 'Name must be at least 2 characters';
13        return null;
14      case 'callsign':
15        if (!value.trim()) return 'Callsign is required';
16        if (!/^[A-Z0-9-]+$/.test(value)) return 'Only uppercase letters, digits, and hyphens';
17        return null;
18      case 'rank':
19        if (!value) return 'Select a rank';
20        return null;
21      default:
22        return null;
23    }
24  };
25
26  const handleBlur = (field) => {
27    setTouched(prev => ({ ...prev, [field]: true }));
28    const error = validate(field, formData[field]);
29    setErrors(prev => ({ ...prev, [field]: error }));
30  };
31
32  const handleChange = (field, value) => {
33    setFormData(prev => ({ ...prev, [field]: value }));
34    // Validate only if the field has been touched
35    if (touched[field]) {
36      const error = validate(field, value);
37      setErrors(prev => ({ ...prev, [field]: error }));
38    }
39  };
40
41  return (
42    <form>
43      <div>
44        <label>Name</label>
45        <input
46          value={formData.name}
47          onChange={(e) => handleChange('name', e.target.value)}
48          onBlur={() => handleBlur('name')}
49          className={errors.name ? 'input-error' : ''}
50        />
51        {touched.name && errors.name && (
52          <p className="field-error">{errors.name}</p>
53        )}
54      </div>
55      {/* Same pattern for remaining fields */}
56    </form>
57  );
58}

Notice the

touched
pattern -- we only show errors after the user has left the field (
onBlur
). We don't want to scare the pilot with alarms before they've even finished entering data!

Handling Server Errors in Forms

The server can return errors that cannot be predicted on the client side -- e.g., "this email is already taken" or "registration limit exceeded".

1function SubmitWithServerErrors() {
2  const [errors, setErrors] = useState({});
3  const [serverError, setServerError] = useState(null);
4
5  const handleSubmit = async (e) => {
6    e.preventDefault();
7    setServerError(null);
8
9    try {
10      const response = await fetch('/api/crew/register', {
11        method: 'POST',
12        headers: { 'Content-Type': 'application/json' },
13        body: JSON.stringify(formData),
14      });
15
16      const data = await response.json();
17
18      if (!response.ok) {
19        if (data.fieldErrors) {
20          // Field-specific errors (e.g., "email already exists")
21          setErrors(data.fieldErrors);
22        }
23        if (data.message) {
24          // General server error
25          setServerError(data.message);
26        }
27        return;
28      }
29
30      // Success - redirect or show message
31    } catch (error) {
32      setServerError('Failed to connect to the base station');
33    }
34  };
35
36  return (
37    <form onSubmit={handleSubmit}>
38      {serverError && (
39        <div className="server-error-banner">{serverError}</div>
40      )}
41      {/* Form fields with field-level errors */}
42    </form>
43  );
44}

try-catch in handleSubmit

Every form submission handler should be wrapped in

try-catch
. Without it, a network error or unexpected server error will cause an unhandled exception.

1const handleSubmit = async (e) => {
2  e.preventDefault();
3  setIsSubmitting(true);
4  setErrors({});
5
6  try {
7    // 1. Local validation
8    const validationErrors = validateAllFields(formData);
9    if (Object.keys(validationErrors).length > 0) {
10      setErrors(validationErrors);
11      return;
12    }
13
14    // 2. Send to server
15    const response = await submitToServer(formData);
16
17    // 3. Handle server response
18    if (!response.ok) {
19      const errorData = await response.json();
20      throw new Error(errorData.message || 'Server error');
21    }
22
23    // 4. Success
24    setSuccess(true);
25  } catch (error) {
26    // 5. Error handling
27    setErrors({ submit: error.message });
28  } finally {
29    // 6. Always executed - disable loader
30    setIsSubmitting(false);
31  }
32};

UX Best Practices for Error Messages

Good error messages in forms should be:

  1. Specific -- "Password must be at least 8 characters" instead of "Invalid password"
  2. Helpful -- tell the user HOW to fix the error
  3. Visible -- display the error near the field it relates to
  4. Gentle -- don't show all errors at once before the first submit
  5. Accessible -- use
    aria-invalid
    and
    aria-describedby
    attributes
1<div className="form-field">
2  <label htmlFor="password">Password</label>
3  <input
4    id="password"
5    type="password"
6    aria-invalid={!!errors.password}
7    aria-describedby={errors.password ? 'password-error' : undefined}
8  />
9  {errors.password && (
10    <p id="password-error" className="field-error" role="alert">
11      {errors.password}
12    </p>
13  )}
14</div>

Remember -- a form is like a spacecraft's control panel. Every error must be communicated clearly and precisely so that the pilot (user) knows exactly what to fix. Never show mysterious error codes -- always translate them into human-readable language.

Go to CodeWorlds