Skip to main content

Error Handling Conventions

AppErrorString conversion

Every Rust command returns Result<T, String>. Internally, repository/service functions return Result<T, AppError> (src/core/src/error.rs), and commands convert errors to plain strings via .map_err(Into::into), relying on:

impl From<AppError> for String {
fn from(e: AppError) -> String {
e.to_string()
}
}

AppError variants and their Display output:

VariantMessage format
Database(sqlx::Error)database error: {0}
Migration(sqlx::migrate::MigrateError)migration error: {0}
NotFound(String)not found: {0}
Validation(String)validation error: {0}
Serialization(serde_json::Error)serialization error: {0}

Some commands (e.g. grade_writing) build ad-hoc String errors directly rather than going through AppError, since they call external libraries (reqwest) whose errors are converted with .map_err(|e| e.to_string()).

How the frontend surfaces errors

Tauri delivers a command's Err value to the frontend's invoke() call, which rejects the returned promise. Per src/ui/lib/tauri.ts conventions, callers should wrap invoke()-based service calls in try/catch and surface the caught string as a toast notification (via sonner/components/ui/toaster), rather than assuming a structured error object.

Common error strings to expect

Error stringSource
"AI grading failed"grade_writing — gateway responded with a non-2xx status
"Invalid AI response format"grade_writing — gateway response wasn't valid GradingResult JSON
"taskType must be 'task1' or 'task2'"grade_writing — invalid task_type input
"userResponse must be under 10000 characters"grade_writing — response too long
"prompt must be under 5000 characters"grade_writing — prompt too long
"Unknown export kind ⟨kind⟩."export_test_to_zip — invalid kind argument
"Unknown import kind ⟨kind⟩."validate_import — invalid kind argument
not found: ...Various — requested id doesn't exist or isn't visible to the caller
validation error: ...Various — malformed input caught before a DB write

Guidance for the frontend service layer

try {
await gradeWriting({ /* ... */ });
} catch (err) {
// err here is the raw string thrown by the Rust command, not an Error instance
toast.error(typeof err === "string" ? err : "Something went wrong");
}

Because errors arrive as raw thrown values (not necessarily Error instances), always guard with a typeof err === "string" (or similar) check before displaying it, rather than assuming err.message exists.