Note err.stack is not standardized but is supported everywhere in practice. It includes the error message and the call chain leading to where the error was created.
// Validate on change, blur, or submitconst errors = {};
if (!value) errors.field = 'Required';
example
function SignupForm() {
const [fields, setFields] = useState({ name: '', email: '', age: '' });
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
function validate(data) {
const errs = {};
if (!data.name.trim()) errs.name = 'Name is required';
if (!data.email.includes('@')) errs.email = 'Invalid email';
if (Number(data.age) < 18) errs.age = 'Must be at least 18';
return errs;
}
function handleChange(field, value) {
const updated = { ...fields, [field]: value };
setFields(updated);
if (touched[field]) {
setErrors(validate(updated));
}
}
function handleBlur(field) {
setTouched(prev => ({ ...prev, [field]: true }));
setErrors(validate(fields));
}
function handleSubmit(e) {
e.preventDefault();
const errs = validate(fields);
setErrors(errs);
setTouched({ name: true, email: true, age: true });
if (Object.keys(errs).length === 0) {
submitSignup(fields);
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={fields.name}
onChange={e => handleChange('name', e.target.value)}
onBlur={() => handleBlur('name')}
/>
{touched.name && errors.name && <span>{errors.name}</span>}
{/* repeat for other fields */}
<button type="submit">Sign Up</button>
</form>
);
}
Note Validate on blur for a good UX: users see errors after leaving a field, not while typing. On submit, validate everything and mark all fields as touched. For complex forms, consider a form library or useReducer.