Form Error
Trace field and form validation from the invalid proxy to SvelteKit's form boundary.What this error means and what can trigger it
FormError is the server-side validation value produced by a SER Form handler. The name is slightly misleading: this is tagged data with _tag: "FormError" and an issues array, not a JavaScript Error or a RuntimeError subclass. SER does not throw it. Instead, each issue carries a message and a path made from string or numeric segments.
In application code, this usually comes from the path-aware invalid proxy passed to a form handler. Calling invalid.email("Required") fails the current effect with an issue at ["email"]; each property access extends that path, so invalid.profile.email("Required") produces ["profile", "email"]. Calling the proxy itself, as in invalid("Could not save"), leaves the path empty and attaches the issue to the whole form.
Exactly where it triggers
return (message: string) =>
Effect.fail(create_form_error([{ message, path: [...path] } satisfies FormIssue]));This is an Effect.fail, not a synchronous throw. The failure becomes observable when the handler effect runs. The same proxy recursively handles nested properties, carrying the accumulated path into the next call.
The value itself is constructed here:
export function create_form_error(issues: readonly FormIssue[]): FormError {
return { _tag: "FormError", issues };
}At the request boundary, SER recognizes the tag and hands the issues to SvelteKit:
const form_error = cause.failureOrCause.pipe(
Option.filter((failure): failure is FormError => is_tagged_failure(failure, "FormError")),
);
if (Option.isSome(form_error)) {
invalid(...form_error.value.issues);
}Why this is the case
Validation is expected control flow inside a form handler. Keeping it in the effect's typed error channel allows the handler to compose validation with database work, services, retries, and ordinary domain failures without throwing an untyped exception through the middle of that program.
SvelteKit's invalid helper belongs at the request boundary because it uses framework control flow to produce the form response. SER therefore keeps validation as plain data until it has the active request and can translate the issue list once. The _schema member on FormError is phantom typing only. It helps TypeScript relate paths to a form schema, but it is absent from the runtime object and never sent over the wire.