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>
);
}