Skip to main content

Error Handling

Handle errors gracefully in your Forma-powered forms.

FormaErrorBoundary​

Wrap forms with FormaErrorBoundary to catch render errors:

MyForm.tsx
import { FormRenderer, FormaErrorBoundary } from "@fogpipe/forma-react";

function MyForm() {
return (
<FormaErrorBoundary
fallback={<div>Something went wrong with the form</div>}
onError={(error) => console.error("Form error:", error)}
>
<FormRenderer
spec={mySpec}
components={components}
onSubmit={handleSubmit}
/>
</FormaErrorBoundary>
);
}

Props​

PropTypeDescription
fallbackReactNode | (error, reset) => ReactNodeUI to show on error
onError(error: Error, errorInfo: React.ErrorInfo) => voidCallback when error occurs
resetKeystring | numberChange to reset error state

Fallback Options​

Static Fallback​

Simple error message:

<FormaErrorBoundary fallback={<div>Form failed to load</div>}>
{/* ... */}
</FormaErrorBoundary>

Function Fallback​

Access error details and reset function:

<FormaErrorBoundary
fallback={(error, reset) => (
<div className="error-container">
<h3>Form Error</h3>
<p>{error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}
>
{/* ... */}
</FormaErrorBoundary>

Error Logging​

Log errors for debugging or monitoring:

<FormaErrorBoundary
onError={(error) => {
// Log to console
console.error("Form rendering error:", error);

// Send to error tracking service
errorTracker.captureException(error, {
context: "forma-form",
formId: spec.meta.id,
});
}}
fallback={<ErrorMessage />}
>
{/* ... */}
</FormaErrorBoundary>

Reset on Spec Change​

Reset error state when the form specification changes:

function DynamicForm({ spec }) {
return (
<FormaErrorBoundary
resetKey={spec.meta.id} // Reset when form changes
fallback={<ErrorMessage />}
>
<FormRenderer spec={spec} components={components} />
</FormaErrorBoundary>
);
}

Validation Errors​

warning

Validation errors are not thrown as exceptions. They are part of the form state and must be read from field.errors or forma.errors. Do not wrap validation handling in try/catch blocks.

Validation errors are different from render errors. They're handled through the form state.

When to Show Errors​

Forma runs validation immediately so that isValid and canProceed are always accurate. This means field.errors is populated before the user interacts with a field.

To avoid showing errors on untouched fields, use field.visibleErrors — it is pre-filtered by interaction state (touched or submitted):

TextInput.tsx
const TextInput = ({ field }: TextComponentProps) => (
<div>
<input
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value)}
onBlur={field.onBlur}
className={field.visibleErrors.length > 0 ? "input-error" : ""}
/>
{field.visibleErrors.map((error, i) => (
<p key={i} className="error-message" role="alert">
{error.message}
</p>
))}
</div>
);
tip

field.errors — all validation errors, always populated (use for programmatic checks). field.visibleErrors — errors filtered by touched/submitted state (use for UI display).

Alternatively, you can gate error display manually with field.touched:

{
field.touched && field.errors.length > 0 && (
<span className="error">{field.errors[0].message}</span>
);
}

Displaying Field Errors​

TextInput.tsx
const TextInput = ({ field }) => (
<div>
<input
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value)}
onBlur={field.onBlur}
className={field.visibleErrors.length > 0 ? "input-error" : ""}
/>
{field.visibleErrors.map((error, i) => (
<p key={i} className="error-message" role="alert">
{error.message}
</p>
))}
</div>
);

Accessing All Errors​

FormErrors.tsx
function FormErrors({ forma }) {
if (forma.errors.length === 0) return null;

return (
<div className="form-errors" role="alert">
<h4>Please fix the following errors:</h4>
<ul>
{forma.errors.map((error, i) => (
<li key={i}>
<strong>{error.field}:</strong> {error.message}
</li>
))}
</ul>
</div>
);
}

Error Severity​

Handle errors and warnings differently:

FieldFeedback.tsx
function FieldFeedback({ errors }) {
const fieldErrors = errors.filter((e) => e.severity === "error");
const warnings = errors.filter((e) => e.severity === "warning");

return (
<>
{fieldErrors.map((error, i) => (
<p key={`error-${i}`} className="error">
{error.message}
</p>
))}
{warnings.map((warning, i) => (
<p key={`warning-${i}`} className="warning">
{warning.message}
</p>
))}
</>
);
}

Submission Errors​

Handle errors during form submission:

SubmitHandler.tsx
function SubmitHandler({ forma }) {
const [submitError, setSubmitError] = useState<string | null>(null);

const handleSubmit = async () => {
setSubmitError(null);

try {
await forma.submitForm();
} catch (error) {
setSubmitError(
error instanceof Error ? error.message : "An unexpected error occurred",
);
}
};

return (
<div>
{submitError && (
<div className="submit-error" role="alert">
{submitError}
</div>
)}

<button onClick={handleSubmit} disabled={forma.isSubmitting}>
{forma.isSubmitting ? "Submitting..." : "Submit"}
</button>
</div>
);
}

Invalid Specification Handling​

Handle cases where the Forma specification might be invalid:

SafeFormRenderer.tsx
import { validateFormaComprehensive } from "@fogpipe/forma-core";

function SafeFormRenderer({ spec, ...props }) {
const validation = useMemo(() => validateFormaComprehensive(spec), [spec]);

if (!validation.valid) {
return (
<div className="spec-error">
<h3>Invalid Form Specification</h3>
<ul>
{validation.errors.map((error, i) => (
<li key={i}>{error.message}</li>
))}
</ul>
</div>
);
}

return (
<FormaErrorBoundary fallback={<div>Form error</div>}>
<FormRenderer spec={spec} {...props} />
</FormaErrorBoundary>
);
}