Profiles·Public

react-hook-form

semver^7.0.0postconditions12functions7last verified2026-06-23coverage score78%

Postconditions — what we check

  • handleSubmit · async-submit-unhandled-error
    error
    WhenonSubmit callback is async and contains operations that can throw (API calls, database operations, etc.) without try-catch wrapping
    ThrowsUnhandledPromiseRejection
    Required handlingMUST wrap async operations in try-catch within onSubmit callback. handleSubmit does NOT catch errors thrown inside onSubmit - errors will become unhandled promise rejections. Correct pattern: const onSubmit = async (data) => { try { await apiCall(data); // success handling } catch (error) { // error handling (toast, setError, etc.) } }; Incorrect pattern: const onSubmit = async (data) => { await apiCall(data); // ❌ No error handling };
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1]
  • handleSubmit · empty-catch-block-silent-failure
    info
    WhenWITHIN a handleSubmit onSubmit callback function, try-catch exists but catch block is empty, only logs to console, or doesn't call setError/toast/user feedback mechanisms
    ThrowsN/A (silent failure - users unaware of submission failure)
    Required handlingIn form submission handlers (onSubmit callbacks), catch blocks MUST provide user feedback about submission failures. This rule ONLY applies to react-hook-form submission contexts. Users should be informed when form submission fails through: - Toast notifications (toast.error, notification APIs) - Form error state via setError() - Alert/modal dialogs - Form-level error display Note: Backend logging code, utility functions, and non-form code should NOT trigger this rule. This is specifically for user-facing form submission error handling. Incorrect patterns (in onSubmit callbacks): catch (error) { } // ❌ Empty catch catch (error) { console.log(error); } // ❌ Only logs (no user feedback) catch (error) { /* TODO: handle */ } // ❌ No implementation Correct patterns (in onSubmit callbacks): catch (error) { toast.error('Failed to submit form'); // ✅ User feedback setError('root', { message: error.message }); } Acceptable patterns (outside form contexts - should NOT trigger): // Logging code catch (error) { console.error('Log failed:', error); } // ✅ OK for loggers // Backend/API code catch (error) { logger.error(error); } // ✅ OK for servers
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[2]
  • handleSubmit · server-validation-error-not-displayed
    warning
    WhenServer/API returns validation errors but setError() is not called to display them to users
    ThrowsN/A (validation feedback missing)
    Required handlingMUST use setError() to display server validation errors to users. When your API returns field-specific errors: const onSubmit = async (data) => { try { const response = await api.submit(data); } catch (error) { if (error.validationErrors) { Object.entries(error.validationErrors).forEach(([field, message]) => { setError(field, { type: 'manual', message }); }); } } }; For global/form-level errors: setError('root.serverError', { type: '400', message: 'Server validation failed' });
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[3]
  • useFormContext · missing-form-provider
    error
    WhenuseFormContext() called in component tree without FormProvider wrapper
    ThrowsTypeError
    Required handlingMUST wrap your form with FormProvider component before using useFormContext in child components. useFormContext returns null when FormProvider is missing, causing TypeError when accessing properties. Correct pattern: import { useForm, FormProvider, useFormContext } from 'react-hook-form'; function ParentForm() { const methods = useForm(); return ( <FormProvider {...methods}> <form> <NestedComponent /> </form> </FormProvider> ); } function NestedComponent() { const { control, formState } = useFormContext(); // ✅ Safe return <input />; } Incorrect pattern: function NestedComponent() { const { control } = useFormContext(); // ❌ Crashes if no provider }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[4]
  • useFormContext · useformcontext-property-access-error
    error
    WhenDestructuring properties from useFormContext without null check when FormProvider might be missing
    ThrowsTypeError (Cannot destructure property 'X' of 'Object(...)' as it is null)
    Required handlingCheck for null before accessing useFormContext properties, or ensure FormProvider is always present. Defensive pattern: const methods = useFormContext(); if (!methods) { throw new Error('Component must be used within FormProvider'); } const { control, formState } = methods; Or add runtime validation: const { control } = useFormContext() ?? {}; if (!control) return null; // or throw error
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[4]
  • useFieldArray · unhandled-field-array-operations
    warning
    WhenField array operations (append, remove, update) fail due to validation errors but error is not handled
    ThrowsValidationError
    Required handlingHandle validation errors when manipulating field arrays. Field array operations respect validation rules. If adding/removing fields violates validation (e.g., min/max array length), handle appropriately. Example: const { append, fields } = useFieldArray({ name: 'items', control }); const handleAdd = () => { try { append({ name: '', value: '' }); } catch (error) { // Handle validation error toast.error('Cannot add more items'); } };
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[5]
  • trigger · trigger-async-validator-throws
    warning
    WhenA field is registered with a custom async validate function (via register(name, { validate })) that throws an error rather than returning a string message. This is common when the validate function makes a network call (e.g., checking email uniqueness via API) and that call fails. trigger() awaits the validate function but has no try-catch: any thrown error propagates directly out of trigger() as an unhandled Promise rejection.
    ThrowsWhatever the custom validator throws — typically Error, TypeError, or network-related errors. Not a react-hook-form specific error type: the thrown value is re-thrown as-is.
    Required handlingWhen using async custom validators that make network calls, wrap the network call in try-catch INSIDE the validator function and return an error message string instead of throwing: register('email', { validate: async (value) => { try { const res = await fetch(`/api/check-email?email=${value}`); const { taken } = await res.json(); return taken ? 'Email already in use' : true; } catch (error) { // ✅ Return message string — do NOT let the error propagate return 'Could not verify email availability'; } } }); If calling trigger() externally and you cannot control all validators: try { const isValid = await trigger('email'); } catch (error) { // ✅ Catch validator errors at the call site console.error('Validation failed:', error); }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[6][7]
  • trigger · trigger-result-not-awaited
    error
    Whentrigger() is called but the returned Promise<boolean> is not awaited before using the validation result to gate further logic. A common antipattern in multi-step forms: trigger the current step's fields, then immediately proceed to the next step without waiting for the async validation to complete. Because trigger() is async (it awaits schema resolvers and custom validators), calling it synchronously gives undefined, not the validation result.
    ThrowsDoes not throw — but the validation result is a pending Promise, so boolean checks like `if (trigger('email'))` always evaluate to truthy (a Promise object is truthy) even when validation fails.
    Required handlingAlways await trigger() before using the result: // Multi-step form — validate before advancing step const handleNextStep = async () => { // ✅ Await the result const isValid = await trigger(['firstName', 'email']); if (!isValid) return; // validation failed — stay on current step setStep(step + 1); }; Incorrect pattern: const isValid = trigger('email'); // ❌ Returns Promise, not boolean if (isValid) { ... } // ❌ Always true (Promise is truthy)
    costmediumin prodsilent failureusers seelost datavisibilitysilent
    Sources[6]
  • Form · form-network-error-no-feedback
    warning
    WhenThe Form component is used with an action prop (which triggers a fetch() call) but without an onError callback. When the network request fails (DNS failure, connection refused, timeout) OR returns an HTTP error status (4xx/5xx), the error is caught internally and hasError is set, but without onError the user receives no feedback. The form's isSubmitSuccessful becomes false and a 'root.server' field error is set, but if no UI renders formState.errors.root.server, the failure is invisible.
    ThrowsDoes not throw externally — the Form component internally catches all fetch errors. The onError callback receives either { response: Response } for HTTP errors or { error: unknown } for network-level failures. Without onError, the error is silently dropped.
    Required handlingAlways provide onError when using Form with action prop to handle both network failures and HTTP error responses: <Form action="/api/submit" method="post" control={control} onSuccess={({ response }) => { toast.success('Form submitted successfully'); }} onError={({ response, error }) => { if (error) { // Network failure (no response) toast.error('Network error — please check your connection'); } else if (response) { // HTTP error response toast.error(`Submission failed: ${response.status} ${response.statusText}`); } }} > {/* fields */} </Form> Alternatively, render formState.errors.root.server in the UI to show server errors: {errors.root?.server && ( <p className="error">Server error: {errors.root.server.type}</p> )}
    costmediumin prodsilent failureusers seelost datavisibilitysilent
    Sources[8]
  • Form · form-onsubmit-unhandled-error
    error
    WhenThe Form component's onSubmit prop is an async function that throws (or rejects), and the error is not caught. The Form component internally wraps onSubmit in control.handleSubmit(), which re-throws errors from the onValid callback at source line 2327: `if (onValidError) { throw onValidError; }`. This error propagates out of the Form's submit handler and becomes an unhandled Promise rejection if React's error boundary or the caller does not catch it.
    ThrowsWhatever the onSubmit callback throws — network errors, API errors, or any Error thrown during async form processing.
    Required handlingWrap async operations in the onSubmit callback with try-catch: <Form action="/api" control={control} onSubmit={async ({ data, event, formData }) => { try { await processFormData(data); } catch (error) { // ✅ Catch inside onSubmit — don't let it propagate toast.error('Form processing failed'); } }} />
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[8]
  • useForm · async-default-values-unhandled-rejection
    warning
    WhenuseForm is initialized with an async defaultValues function that makes a network call (e.g., fetching user profile data from an API), and that call fails. The async defaultValues function throws or its Promise rejects. React Hook Form does not document an error callback for this case and internally has no try-catch around the async defaultValues invocation. The result: the form remains in isLoading state, never displays default values, and the unhandled rejection may crash the component or produce silent empty form behavior.
    ThrowsUnhandled Promise rejection propagating from the async defaultValues function. Callers see: formState.isLoading stays true, no form defaults are populated, and the rejection may surface as React "Unhandled Rejection" in dev mode.
    Required handlingWrap the async defaultValues fetch in a try-catch and provide fallback values: const { register, handleSubmit, formState: { isLoading } } = useForm({ defaultValues: async () => { try { const response = await fetch('/api/user/profile'); if (!response.ok) throw new Error('Failed to load profile'); return await response.json(); } catch (error) { // ✅ Return safe fallback values instead of rejecting console.error('Could not load default values:', error); return { name: '', email: '' }; // Fallback defaults } } }); // Always show loading state to users if (isLoading) return <Spinner />;
    costmediumin prodsilent failureusers seelost datavisibilitysilent
    Sources[8]
  • useController · controller-onchange-resolver-rejection
    warning
    WhenA custom `resolver` (zodResolver, yupResolver, joiResolver, or a hand-written resolver) is passed to useForm, AND the resolver implementation can throw synchronously or return a rejected Promise (e.g. it makes a network call to a backend validator). When the controlled field's `field.onChange` runs, the library awaits `_runSchema()` which calls the resolver without a try-catch. A resolver that throws — or a `props.validate` form-level validator that throws — causes the returned Promise from `field.onChange` to reject. If the caller writes `await field.onChange(value)` inside an async callback (or chains `.then(...)` without `.catch(...)`), the rejection becomes an unhandled rejection that React surfaces as a "Unhandled Promise rejection" in dev mode and can crash boundaryless components in production.
    ThrowsWhatever the resolver or form-level `validate` function throws — typically Error, TypeError, ZodError-wrapped throws, or network-related errors when the resolver makes an async backend call. Not a react-hook-form-specific error class: the thrown value is re-raised as-is from inside `_runSchema()` or `validateForm()`.
    Required handlingWhen awaiting `field.onChange(...)` (the return value restored in v7.79.0, also true for `register(name).onChange(event)` per UseFormRegisterReturn), wrap the await in try-catch and surface the failure to the user: const { field } = useController({ name: 'email', control }); const onCustomChange = async (value: string) => { try { await field.onChange(value); } catch (error) { // ✅ Catch resolver/validator rejections — common when the resolver // makes a backend call (e.g. async uniqueness check) toast.error('Could not validate field'); } }; If the resolver itself can throw (network-call inside a custom resolver), prefer wrapping inside the resolver so failures are converted to validation errors rather than rejections: const resolver: Resolver = async (values, ctx, options) => { try { const result = await zodResolver(schema)(values, ctx, options); return result; } catch (error) { // ✅ Convert to a structured validation result instead of throwing return { values: {}, errors: { root: { type: 'resolver', message: 'Validation service unreachable' } } }; } }; Same pattern applies to form-level `validate` passed via UseFormProps — catch inside the validator and return `{ root: {...} }` rather than throw. Incorrect pattern (the rejection surfaces as unhandled): const onCustomChange = (value: string) => { field.onChange(value); // ❌ Promise rejection silently unhandled // if resolver throws }; const onAwaited = async (value: string) => { await field.onChange(value); // ❌ No try-catch around await };
    costlowin prodsilent failureusers seelost datavisibilitysilent
    Sources[9][10]

Sources

Every postcondition cites at least one of these. Numbered to match the footnotes above.

  1. [1]react-hook-form.com/docs/useformhttps://react-hook-form.com/docs/useform/handlesubmit
  2. [2]react-hook-form.com/advanced-usagehttps://react-hook-form.com/advanced-usage
  3. [3]react-hook-form.com/docs/useformhttps://react-hook-form.com/docs/useform/seterror
  4. [4]react-hook-form.com/docs/useformcontexthttps://react-hook-form.com/docs/useformcontext
  5. [5]react-hook-form.com/docs/usefieldarrayhttps://react-hook-form.com/docs/usefieldarray
  6. [6]react-hook-form.com/docs/useformhttps://react-hook-form.com/docs/useform/trigger
  7. [7]react-hook-form.com/docs/useformhttps://react-hook-form.com/docs/useform/register
  8. [8]react-hook-form.com/docs/useformhttps://react-hook-form.com/docs/useform
  9. [9]react-hook-form.com/docs/usecontrollerhttps://react-hook-form.com/docs/usecontroller
  10. [10]github.com/react-hook-form/react-hook-formhttps://github.com/react-hook-form/react-hook-form/pull/13518
Need a different package?
Request a profile