The following code is a class that can be used to streamline error handling with Superforms. It is designed to be used with the SaveResult type, which is a promise that resolves to either the saved object or a SaveError object. The SaveError object can be used to provide a more detailed error message to the user, including the field that caused the error and the HTTP status code.
The static from method can be used to handle other types of errors that occur during the save process and convert them to SaveError objects.
The toForm method can be returned from the form action to display the error message to the user.
ts
import { dev } from "$app/environment";import { type NumericRange } from "@sveltejs/kit";import { message, setError, type FormPathLeavesWithErrors, type SuperValidated } from "sveltekit-superforms";export type SaveResult<T extends object | null, S extends Record<string, unknown>> = Promise<T | SaveError<S>>;export class SaveError<TOut extends Record<string, unknown>, TIn extends Record<string, unknown> = TOut> { public status: NumericRange<400, 599> = 500; constructor( public error: string, protected options?: Partial<{ field: FormPathLeavesWithErrors<TOut>; status: NumericRange<400, 599>; }> ) { if (options?.status) this.status = options.status; } static from<TOut extends Record<string, unknown>, TIn extends Record<string, unknown> = TOut>( err: SaveError<TOut, TIn> | Error | unknown ) { if (dev) console.error(err); if (err instanceof SaveError) return err; if (typeof err === "string") return new SaveError<TOut, TIn>(err); if (err && typeof err === "object" && "message" in err && typeof err.message === "string") { return new SaveError<TOut, TIn>(err.message); } return new SaveError<TOut, TIn>("An unknown error has occurred."); } toForm(form: SuperValidated<TOut, App.Superforms.Message, TIn>) { return this.options?.field ? setError(form, this.options.field, this.error, { status: this.status }) : message( form, { type: "error", text: this.error }, { status: this.status } ); }}
Using SaveResult and SaveError
The following code demonstrates how to use the SaveResult type and the SaveError class in a function that saves a log entry for a character. The function returns an object that matches the defined return type in SaveResult (the first type argument) or a SaveError object with the defined schema (the second type argument).
ts
import { SaveError, type SaveResult } from "$lib/util";classLogError extends SaveError<LogSchema> {}export type SaveLogResult = ReturnType<typeof saveLog>;export async function saveLog(input: LogSchema, user?: Session["user"]):SaveResult<LogData, LogSchema> { try { if (!user?.name || !user?.id) throw newLogError("Not authenticated", { status: 401 }); const userId = user.id; const { success } = await rateLimiter(input.id ? "insert" : "update", user.id); if (!success) throw newLogError("Too many requests", { status: 429 }); ... // An error can also be tied to a specific field if (input.isDmLog && !character) throw newLogError("Character not found", { status: 404, field: "characterId" }); ... return log; // The type returned is either LogData } catch (err) { returnLogError.from(err); // or SaveError<LogSchema> }}
Returning the Error to Superforms
The following code demonstrates how to use the SaveError class in a form action. The saveLog action saves a log entry for a character. If the save is successful, the user is redirected to the character page. If there is an error, the SaveError object’s toForm method is called to return the error message to the user.
ts
export const actions = { saveLog: async (event) => { const session = await event.locals.session; if (!session?.user) redirect(302, "/"); const character = await getCharacterCache(event.params.characterId || ""); if (!character) redirect(302, "/characters"); const log = await getLog(event.params.logId, session.user.id, character.id); if (event.params.logId !== "new" && !log.id) redirect(302, `/characters/${character.id}`); const form = await superValidate(event, valibot(characterLogSchema(character))); if (!form.valid) return fail(400, { form }); const result = await saveLog(form.data, session.user); if ("error" in result) return result.toForm(form); redirect(302, `/characters/${character.id}`); }};
Using Without Superforms
You can also use the SaveError class to handle errors in forms that do not use Superforms. The following code demonstrates how to use the SaveError class in a form action that deletes a dungeon master. If an error occurs, SvelteKit’s fail function is called to return the error message to the form.
ts
export const actions = { deleteDM: async (event) => { const session = await event.locals.session; if (!session?.user) redirect(302, "/"); const dms = await getUserDMsWithLogsCache(session.user); const dm = dms.find((dm) => dm.id == event.params.dmId); if (!dm) redirect(302, "/dms"); if (dm.logs.length) return fail(400, { error: "Cannot delete a DM with logs" }); const result = await deleteDM(event.params.dmId, session.user); if ("error" in result) return fail(result.status, { error: result.error }); redirect(302, `/dms`); }};
svelte
<form method="POST" action={`?/deleteDM`} class="flex flex-col items-center gap-4 py-20" use:enhance={({ cancel }) => { if (!confirm(`Are you sure you want to delete ${data.name}? This action cannot be reversed.`)) return cancel(); $pageLoader = true; return async ({ result }) => { await applyAction(result); if (form?.error) { // The error returned in the fail function errorToast(form.error); $pageLoader = false; } else { successToast(`${data.name} deleted`); $searchData = []; } }; }}> <p>This DM has no logs.</p> <button type="submit" class="btn btn-error btn-sm hover:font-bold hover:text-white" aria-label="Delete DM"> Delete DM </button></form>