All tests run on an 8-year-old MacBook Air.
All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.
The first Tauri app I shipped had inconsistent error handling. Some commands returned strings. Some panicked. Some silently swallowed errors.
Here's the pattern I settled on after 7 apps.
The problem with naive error handling
Tauri commands return Result where E must implement serde::Serialize. The temptation is to just return String for errors:
rust#[tauri::command]
fn do_something() -> Result {
some_operation().map_err(|e| e.to_string())
}
This works. It's also a mess at scale. The frontend gets an untyped string. You can't match on error types. Logging is inconsistent. Error messages are whatever .to_string() produces.
The pattern that works
A single app-wide error type:
rust#[derive(Debug, thiserror::Error, serde::Serialize)]
[serde(tag = "kind", content = "message")]
pub enum AppError {
#[error("IO error: {0}")]
Io(String),
#[error("ADB error: {0}")]
Adb(String),
#[error("Database error: {0}")]
Database(String),
#[error("Permission denied: {0}")]
Permission(String),
}
impl Fromstd::io::Error for AppError {
fn from(e: std::io::Error) -> Self {
AppError::Io(e.to_string())
}
}
Every command returns Result. The frontend receives a typed error object with kind and message fields. You can match on kind in TypeScript and show appropriate UI for each error type.
The frontend side
typescripttry {
await invoke('do_something')
} catch (e: any) {
if (e.kind === 'Permission') {
showPermissionDialog()
} else if (e.kind === 'Adb') {
showAdbTroubleshooting()
} else {
showGenericError(e.message)
}
}
Typed errors on both sides. No string parsing. No guessing what went wrong.
The logging layer
Add logging at the command boundary, not scattered through business logic:
rust#[tauri::command]
async fn sync_files(handle: AppHandle) -> Result {
sync_files_inner(&handle).await.map_err(|e| {
log::error!("sync_files failed: {:?}", e);
e
})
}
One log line per command failure. Consistent format. Easy to find in production logs.
The verdict
The thiserror + tagged enum pattern is the correct default for Tauri app error handling. Set it up on day one. Retrofitting consistent error handling into a shipping app is painful.
The String error shortcut is fine for prototypes. Not for anything users will actually run.
If this was useful, a ❤️ helps more than you'd think — thanks!
Hiyoko PDF Vault → https://hiyokoko.gumroad.com/l/HiyokoPDFVault
X → @hiyoyok
Top comments (0)