Rust Error Handling in Tauri Commands — The Pattern That Actually Works

Published: (May 12, 2026 at 08:16 AM EDT)
2 min read
Source: Dev.to

Source: Dev.to

Cover image for Rust Error Handling in Tauri Commands — The Pattern That Actually Works

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 a String for errors:

#[tauri::command]
fn do_something() -> Result {
    some_operation().map_err(|e| e.to_string())
}

This works, but it becomes a mess at scale:

  • The frontend receives 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

#[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 From 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, allowing you to match on kind in TypeScript and show appropriate UI for each error type.

The frontend side

try {
  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:

#[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, but not for anything users will actually run.

If this was useful, a ❤️ helps more than you’d think — thanks!

Links

  • Hiyoko PDF Vault →
  • X →
0 views
Back to Blog

Related posts

Read more »

[Boost]

Python Decorators: From Basics to Real-World Use Cases DigitalOcean for DigitalOcean May 12 ai python tutorial learning 2 reactions Add Comment 11 min read...