概要

TauriのCommandsにおけるエラーハンドリングは、フロントエンドとバックエンド間の通信で発生するエラーを適切に処理するための仕組みである。

RustのResult型を使用することにより、型安全なエラー処理を実現し、エラーの原因を明確に特定できる。

Commandsのエラーハンドリングでは、エラー型のシリアライズ、エラーメッセージの変換、フロントエンドでのエラー受信等が重要な要素となる。

適切なエラーハンドリングにより、ユーザに分かりやすいエラーメッセージを表示し、デバッグを容易にすることができる。

エラーハンドリングの主な特徴は以下の通りである。

  • 型安全なエラー処理
    Rustの型システムにより、コンパイル時にエラー処理の漏れを防ぐことができる。
  • 構造化されたエラー情報
    エラーの種類、メッセージ、コンテキスト情報を含む詳細なエラーを返却できる。
  • フロントエンドとの統合
    TypeScript側でcatchブロックを使用してエラーを処理できる。
  • カスタムエラー型
    アプリケーション固有のエラー型を定義して、エラーを分類できる。



前提条件

エラーハンドリングを定義するには、以下に示す前提条件を満たしている必要がある。

必要な依存関係

Cargo.tomlファイルに以下に示す依存関係を追加する。

 [dependencies]
 tauri = { version = "2", features = ["unstable"] }
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 thiserror = "1"     # カスタムエラー型の定義用
 anyhow = "1"        # 汎用エラーハンドリング用


エラー処理の基本概念

下表に、Rustにおけるエラー処理の概念を示す。

Rustのエラー処理の基本
説明 使用場面
Result<T, E> 成功または失敗を表す。 回復可能なエラー
Option<T> 値の有無を表す。 値が存在しない可能性
panic! プログラムを強制終了 回復不可能なエラー



Result<T, E>による返却

TauriのCommandsでは、Result<T, E> 型を使用してエラーを返却する。

基本的なエラー返却

最も簡単ななエラー返却の例を以下に示す。

 #[tauri::command]
 fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
       Err("Division by zero".to_string())
    }
    else {
       Ok(a / b)
    }
 }


 // TypeScript側でのエラー処理
 
 import { invoke } from '@tauri-apps/api/core'
 
 const divide = async (a: number, b: number): Promise<number> => {
   try {
     const result = await invoke<number>('divide', { a, b })
     console.log('Result:', result)
     return result
   }
   catch (error) {
     console.error('Error:', error)
     throw error
   }
 }


成功 / 失敗のパターン

Result型を使用した成功と失敗のパターンを以下に示す。

 use serde::Serialize;
 
 #[derive(Serialize)]
 struct User {
    id: u32,
    name: String,
    email: String,
 }
 
 #[tauri::command]
 fn get_user(user_id: u32) -> Result<User, String> {
    // データベースからユーザを取得する処理
    if user_id == 0 {
       Err("Invalid user ID".to_string())
    }
    else if user_id > 100 {
       Err("User not found".to_string())
    }
    else {
       Ok(User {
          id: user_id,
          name: format!("User {}", user_id),
          email: format!("user{}@example.com", user_id),
       })
    }
 }


Option型との使い分け

Result型とOption型の使い分けを示す。

 // Option型 : 値が存在しない可能性がある場合
 
 #[tauri::command]
 fn find_user_by_email(email: String) -> Option<User> {
    // ユーザが見つからないことはエラーではなく、通常の状態として扱う
    users.iter().find(|u| u.email == email).cloned()
 }
 
 // Result型 : エラーとして扱うべき場合
 #[tauri::command]
 fn get_user_by_id(user_id: u32) -> Result<User, String> {
    users.iter()
       .find(|u| u.id == user_id)
       .cloned()
       .ok_or_else(|| format!("User with ID {} not found", user_id))
 }


 // TypeScript側での処理
 // Option型の処理
 const findUser = async (email: string) => {
   const user = await invoke<User | null>('find_user_by_email', { email })
   if (user) {
     console.log('Found:', user)
   }
   else {
     console.log('User not found')
   }
 }
 
 // Result型の処理
 const getUser = async (userId: number) => {
   try {
     const user = await invoke<User>('get_user_by_id', { userId })
     console.log('Found:', user)
   }
   catch (error) {
     console.error('Error:', error)
   }
 }



serde::Serializeの制約

Commandからエラーを返却する場合、エラー型は serde::Serialize を定義している必要がある。

エラー型のシリアライズ要件

エラー型をシリアライズ可能にする基本的な実装を以下に示す。

 use serde::Serialize;
 
 #[derive(Debug, Serialize)]
 struct AppError {
    code: u32,
    message: String,
 }
 
 impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
       write!(f, "[{}] {}", self.code, self.message)
    }
 }
 
 impl std::error::Error for AppError {}
 
 #[tauri::command]
 fn validate_input(input: String) -> Result<String, AppError> {
    if input.is_empty() {
       Err(AppError {
          code: 400,
          message: "Input cannot be empty".to_string(),
       })
    }
    else if input.len() > 100 {
       Err(AppError {
          code: 400,
          message: "Input too long".to_string(),
       })
    }
    else {
       Ok(format!("Processed: {}", input))
    }
 }


Serialize実装の例

カスタムエラー型にSerializeを定義する例を以下に示す。

 use serde::{Serialize, Serializer};
 
 #[derive(Debug)]
 pub enum DatabaseError {
    ConnectionFailed(String),
    QueryError(String),
    NotFound,
 }
 
 impl Serialize for DatabaseError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
       S: Serializer,
    {
       match self {
          DatabaseError::ConnectionFailed(msg) => {
             serializer.serialize_str(&format!("Connection failed: {}", msg))
          }
          
          DatabaseError::QueryError(msg) => {
             serializer.serialize_str(&format!("Query error: {}", msg))
          }
          
          DatabaseError::NotFound => {
             serializer.serialize_str("Record not found")
          }
       }
    }
 }
 
 impl std::fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
       match self {
          DatabaseError::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg),
          DatabaseError::QueryError(msg) => write!(f, "Query error: {}", msg),
          DatabaseError::NotFound => write!(f, "Record not found"),
       }
    }
 }


カスタムシリアライザ

より複雑なシリアライズが必要な場合は、カスタムシリアライザを使用する。

 use serde::{Serialize, Serializer, ser::SerializeStruct};
 
 #[derive(Debug)]
 pub struct DetailedError {
    pub kind: ErrorKind,
    pub message: String,
    pub context: Option<String>,
 }
 
 #[derive(Debug)]
 pub enum ErrorKind {
    Validation,
    Database,
    Network,
    Permission,
 }
 
 impl Serialize for DetailedError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
       S: Serializer,
    {
       let mut state = serializer.serialize_struct("DetailedError", 3)?;
       state.serialize_field("kind", &format!("{:?}", self.kind))?;
       state.serialize_field("message", &self.message)?;
       state.serialize_field("context", &self.context)?;
       state.end()
    }
 }


 // TypeScript側での受信
 
 interface DetailedError {
   kind: 'Validation' | 'Database' | 'Network' | 'Permission'
   message: string
   context: string | null
 }
 
 const handleOperation = async () => {
   try {
     await invoke('some_operation')
   }
   catch (error) {
     const detailedError = error as DetailedError
     console.log('Error kind:', detailedError.kind)
     console.log('Message:', detailedError.message)
     if (detailedError.context) {
       console.log('Context:', detailedError.context)
     }
   }
 }



map_errによるエラー変換

map_err メソッドを使用して、エラー型を変換することができる。

エラー型の変換

標準ライブラリのエラーをアプリケーション独自のエラーに変換する例を以下に示す。

 use std::io;
 
 #[derive(Debug, serde::Serialize)]
 pub enum AppError {
    Io(String),
    Parse(String),
    Custom(String),
 }
 
 #[tauri::command]
 fn read_config_file(path: String) -> Result<String, AppError> {
    std::fs::read_to_string(&path)
       .map_err(|e: io::Error| AppError::Io(e.to_string()))?;
 
    // 設定ファイルのパース処理
    let config = parse_config(&path)
       .map_err(|e: String| AppError::Parse(e))?;
 
    Ok(config)
 }
 
 fn parse_config(path: &str) -> Result<String, String> {
    // パース処理
    Ok("parsed config".to_string())
 }


ユーザフレンドリーなエラーメッセージ

技術的なエラーをユーザに分かりやすいメッセージに変換する。

 #[tauri::command]
 fn save_user_data(data: String) -> Result<(), String> {
    std::fs::write("user_data.json", &data)
       .map_err(|e| {
          match e.kind() {
             std::io::ErrorKind::PermissionDenied => {
                "ファイルへの書き込み権限がありません".to_string()
             }
 
             std::io::ErrorKind::NotFound => {
                "指定されたディレクトリが見つかりません".to_string()
             }
 
             std::io::ErrorKind::StorageFull => {
                "ディスクの空き容量が不足しています".to_string()
             }
             _ => format!("ファイルの保存に失敗しました: {}", e)
          }
       })?;
    
    Ok(())
 }


エラーチェーンの構築

エラーの原因を追跡するために、エラーチェーンを構築する。

 #[derive(Debug, serde::Serialize)]
 struct ChainedError {
    message: String,
    source: Option<String>,
 }
 
 #[tauri::command]
 fn complex_operation() -> Result<String, ChainedError> {
    let result = step1()
       .map_err(|e| ChainedError {
          message: "Step 1 failed".to_string(),
          source: Some(e),
       })?;
 
    let result = step2(&result)
       .map_err(|e| ChainedError {
          message: "Step 2 failed".to_string(),
          source: Some(e),
       })?;
 
    Ok(result)
 }
 
 fn step1() -> Result<String, String> {
    Ok("step1 result".to_string())
 }
 
 fn step2(input: &str) -> Result<String, String> {
    Ok(format!("{} + step2", input))
 }



フロントエンドでのcatch処理

TypeScript側でのエラー処理の定義パターンを示す。

TypeScriptでのエラー処理

基本的なtry-catchパターンを以下に示す。

 import { invoke } from '@tauri-apps/api/core'
 
 interface User {
   id: number
   name: string
 }
 
 const fetchUser = async (userId: number): Promise<User | null> => {
   try {
     const user = await invoke<User>('get_user', { userId })
     return user
   }
   catch (error) {
     // エラーは通常、文字列として送信される
     const errorMessage = error as string
     console.error('Failed to fetch user:', errorMessage)
 
     // エラーの種類に応じた処理
     if (errorMessage.includes('not found')) {
       console.log('User does not exist')
     }
     else if (errorMessage.includes('Invalid')) {
       console.log('Invalid user ID')
     }
 
     return null
   }
 }


try-catchパターン

複数のCommandを実行する場合のパターンを以下に示す。

 import { invoke } from '@tauri-apps/api/core'
 
 interface OperationResult {
   success: boolean
   data?: unknown
   error?: string
 }
 
 const performOperations = async (): Promise<OperationResult> => {
   try {
     // 複数の操作を実行
     const user = await invoke('get_current_user')
     const settings = await invoke('get_user_settings', { userId: user.id })
     const data = await invoke('fetch_user_data', { settings })
 
     return {
       success: true,
       data: { user, settings, data }
     }
   }
   catch (error) {
     return {
       success: false,
       error: error as string
     }
   }
 }


エラー表示のベストプラクティス

Reactでのエラー表示コンポーネントの例を以下に示す。

 import { useState, ReactNode } from 'react'
 import { invoke } from '@tauri-apps/api/core'
 
 interface ErrorBoundaryState {
   hasError: boolean
   error: string | null
 }
 
 interface ErrorDisplayProps {
   error: string
   onRetry?: () => void
   onDismiss?: () => void
 }
 
 function ErrorDisplay({ error, onRetry, onDismiss }: ErrorDisplayProps) {
   return (
     <div
       style={{
         padding: '16px',
         backgroundColor: '#fee',
         border: '1px solid #f88',
         borderRadius: '4px',
         marginBottom: '16px'
       }}
     >
       <div style={{ display: 'flex', alignItems: 'start', gap: '8px' }}>
         <span style={{ color: '#c00', fontSize: '20px' }}></span>
         <div style={{ flex: 1 }}>
           <h4 style={{ margin: '0 0 8px 0', color: '#c00' }}>エラーが発生しました</h4>
           <p style={{ margin: 0, color: '#600' }}>{error}</p>
         </div>
         {onDismiss && (
           <button
             onClick={onDismiss}
             style={{
               background: 'none',
               border: 'none',
               fontSize: '20px',
               cursor: 'pointer'
             }}
           >
             ×
           </button>
         )}
       </div>
       {onRetry && (
         <button
           onClick={onRetry}
           style={{
             marginTop: '12px',
             padding: '8px 16px',
             backgroundColor: '#c00',
             color: 'white',
             border: 'none',
             borderRadius: '4px',
             cursor: 'pointer'
           }}
         >
           再試行
         </button>
       )}
     </div>
   )
 }
 
 function DataFetcher() {
   const [data, setData] = useState<string | null>(null)
   const [error, setError] = useState<string | null>(null)
   const [loading, setLoading] = useState(false)
 
   const fetchData = async () => {
     setLoading(true)
     setError(null)
 
     try {
       const result = await invoke<string>('fetch_data')
       setData(result)
     }
     catch (err) {
       setError(err as string)
     }
     finally {
       setLoading(false)
     }
   }
 
   return (
     <div>
       <h2>Data Fetcher</h2>
 
       {error && (
         <ErrorDisplay
           error={error}
           onRetry={fetchData}
           onDismiss={() => setError(null)}
         />
       )}
 
       {loading && <p>Loading...</p>}
 
       {data && (
         <div style={{ padding: '16px', backgroundColor: '#f5f5f5' }}>
           {data}
         </div>
       )}
 
       <button onClick={fetchData} disabled={loading}>
         {loading ? 'Fetching...' : 'Fetch Data'}
       </button>
     </div>
   )
 }
 
 export default DataFetcher



カスタムエラー型の定義

アプリケーション固有のエラー型を定義することで、エラー処理を体系化できる。

thiserrorクレートの使用

thiserror クレートを使用することにより、簡単にカスタムエラー型を定義できる。

 use thiserror::Error;
 use serde::Serialize;
 
 #[derive(Debug, Error)]
 pub enum AppError {
    #[error("User not found: {0}")]
    UserNotFound(u32),
 
    #[error("Invalid input: {0}")]
    InvalidInput(String),
 
    #[error("Database error: {0}")]
    DatabaseError(String),
 
    #[error("Permission denied")]
    PermissionDenied,
 
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
 
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),
 }
 
 // Serialize実装
 impl Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
       S: serde::Serializer,
    {
       serializer.serialize_str(&self.to_string())
    }
 }
 
 // 使用例
 #[tauri::command]
 fn get_user(user_id: u32) -> Result<User, AppError> {
    if user_id == 0 {
       return Err(AppError::InvalidInput("User ID cannot be 0".to_string()))
    }
 
    let user = database::find_user(user_id)
       .map_err(|e| AppError::DatabaseError(e.to_string()))?
       .ok_or(AppError::UserNotFound(user_id))?;
 
    Ok(user)
 }


anyhowクレートの使用

anyhow クレートは、汎用的なエラーハンドリングに適している。

 use anyhow::{Context, Result};
 
 #[tauri::command]
 fn read_config() -> Result<String, String> {
    let content = std::fs::read_to_string("config.json")
       .context("Failed to read config file")
       .map_err(|e| e.to_string())?;
 
    let config: Config = serde_json::from_str(&content)
       .context("Failed to parse config")
       .map_err(|e| e.to_string())?;
 
    Ok(config.name)
 }
 
 // context()メソッドでエラーにコンテキストを追加
 #[tauri::command]
 fn process_file(path: String) -> Result<(), String> {
    let content = std::fs::read_to_string(&path)
       .with_context(|| format!("Failed to read file: {}", path))
       .map_err(|e| e.to_string())?;
 
    // ...処理
 
    Ok(())
 }


独自エラー型の設計

アプリケーション固有のエラー型を設計する例を以下に示す。

 use serde::Serialize;
 
 #[derive(Debug, Clone, Serialize)]
 #[serde(tag = "type", content = "details")]
 pub enum ApiError {
    // バリデーションエラー
    Validation { field: String, message: String },
 
    // 認証エラー
    Authentication { reason: String },
 
    // 認可エラー
    Authorization { resource: String, action: String },
 
    // リソースエラー
    NotFound { resource: String, id: String },
 
    // サーバエラー
    Internal { message: String },
 
    // 外部サービスエラー
    ExternalService { service: String, error: String },
 }
 
 impl std::fmt::Display for ApiError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
       match self {
          ApiError::Validation { field, message } => {
             write!(f, "Validation error on '{}': {}", field, message)
          }
 
          ApiError::Authentication { reason } => {
             write!(f, "Authentication failed: {}", reason)
          }
 
          ApiError::Authorization { resource, action } => {
             write!(f, "Not authorized to {} on {}", action, resource)
          }
 
          ApiError::NotFound { resource, id } => {
             write!(f, "{} with id '{}' not found", resource, id)
          }
 
          ApiError::Internal { message } => {
             write!(f, "Internal server error: {}", message)
          }
 
          ApiError::ExternalService { service, error } => {
             write!(f, "External service '{}' error: {}", service, error)
          }
       }
    }
 }
 
 // 使用例
 #[tauri::command]
 fn update_user(user_id: u32, data: UpdateData) -> Result<User, ApiError> {
    // バリデーション
    if data.name.is_empty() {
       return Err(ApiError::Validation {
          field: "name".to_string(),
          message: "Name cannot be empty".to_string(),
       })
    }
 
    // ユーザ取得
    let user = database::find_user(user_id)
       .map_err(|e| ApiError::Internal { message: e.to_string() })?
       .ok_or(ApiError::NotFound {
          resource: "User".to_string(),
          id: user_id.to_string(),
       })?;
 
    // 更新処理
    let updated = database::update_user(user_id, data)
       .map_err(|e| ApiError::Internal { message: e.to_string() })?;
 
    Ok(updated)
 }


 // TypeScript側での型定義
 
 interface ApiError {
   type: 'Validation' | 'Authentication' | 'Authorization' | 'NotFound' | 'Internal' | 'ExternalService'
   details: ValidationDetails | string | NotFoundDetails | InternalDetails | ExternalServiceDetails
 }
 
 interface ValidationDetails {
   field: string
   message: string
 }
 
 interface NotFoundDetails {
   resource: string
   id: string
 }
 
 interface InternalDetails {
   message: string
 }
 
 interface ExternalServiceDetails {
   service: string
   error: string
 }
 
 const handleApiError = (error: ApiError) => {
   switch (error.type) {
     case 'Validation':
       console.error(`Validation error on ${(error.details as ValidationDetails).field}`)
       break
     case 'Authentication':
       console.error('Please log in again')
       break
     case 'Authorization':
       console.error('You do not have permission')
       break
     case 'NotFound':
       const notFound = error.details as NotFoundDetails
       console.error(`${notFound.resource} not found`)
       break
     case 'Internal':
       console.error('Server error occurred')
       break
     case 'ExternalService':
       console.error('External service unavailable')
       break
   }
 }



サンプルコード

エラーハンドリング定義の例を以下に示す。

Rust側の完全実装

 // src-tauri/src/error.rs
 use serde::Serialize;
 use thiserror::Error;
 
 #[derive(Debug, Error, Serialize)]
 pub enum TodoError {
    #[error("Todo not found: {0}")]
    NotFound(u32),
 
    #[error("Invalid title: {0}")]
    InvalidTitle(String),
 
    #[error("Database error: {0}")]
    Database(String),
 
    #[error("IO error: {0}")]
    Io(String),
 }
 
 impl From<std::io::Error> for TodoError {
    fn from(e: std::io::Error) -> Self {
       TodoError::Io(e.to_string())
    }
 }
 
 impl From<rusqlite::Error> for TodoError {
    fn from(e: rusqlite::Error) -> Self {
       TodoError::Database(e.to_string())
    }
 }
 
 // src-tauri/src/commands/todo.rs
 use crate::error::TodoError;
 use serde::{Deserialize, Serialize};
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Todo {
    pub id: u32,
    pub title: String,
    pub completed: bool,
 }
 
 #[tauri::command]
 pub fn get_todos() -> Result<Vec<Todo>, TodoError> {
    // データベースから取得
    let todos = vec![
       Todo { id: 1, title: "Learn Tauri".to_string(), completed: false },
       Todo { id: 2, title: "Build app".to_string(), completed: false },
    ];
    Ok(todos)
 }
 
 #[tauri::command]
 pub fn get_todo(id: u32) -> Result<Todo, TodoError> {
    let todos = get_todos()?;
    todos.into_iter()
       .find(|t| t.id == id)
       .ok_or(TodoError::NotFound(id))
 }
 
 #[tauri::command]
 pub fn create_todo(title: String) -> Result<Todo, TodoError> {
    if title.trim().is_empty() {
       return Err(TodoError::InvalidTitle("Title cannot be empty".to_string()))
    }
 
    if title.len() > 100 {
       return Err(TodoError::InvalidTitle("Title too long (max 100 chars)".to_string()))
    }
 
    Ok(Todo {
       id: rand::random(),
       title,
       completed: false,
    })
 }
 
 #[tauri::command]
 pub fn update_todo(id: u32, title: Option<String>, completed: Option<bool>) -> Result<Todo, TodoError> {
    let mut todo = get_todo(id)?;
 
    if let Some(t) = title {
       if t.trim().is_empty() {
          return Err(TodoError::InvalidTitle("Title cannot be empty".to_string()))
       }
       todo.title = t;
    }
 
    if let Some(c) = completed {
       todo.completed = c;
    }
 
    Ok(todo)
 }
 
 #[tauri::command]
 pub fn delete_todo(id: u32) -> Result<(), TodoError> {
    // 存在確認
    get_todo(id)?;
 
    // 削除処理
    Ok(())
 }


Reactでのエラー表示コンポーネント

 // src/components/TodoApp.tsx
 import { useState, useEffect } from 'react'
 import { invoke } from '@tauri-apps/api/core'
 
 interface Todo {
   id: number
   title: string
   completed: boolean
 }
 
 interface TodoError {
   type: 'NotFound' | 'InvalidTitle' | 'Database' | 'Io'
   details: string | number
 }
 
 function TodoApp() {
   const [todos, setTodos] = useState<Todo[]>([])
   const [newTodo, setNewTodo] = useState('')
   const [loading, setLoading] = useState(false)
   const [error, setError] = useState<TodoError | null>(null)
 
   useEffect(() => {
     loadTodos()
   }, [])
 
   const loadTodos = async () => {
     setLoading(true)
     setError(null)
     try {
       const result = await invoke<Todo[]>('get_todos')
       setTodos(result)
     }
     catch (err) {
       setError(err as TodoError)
     }
     finally {
       setLoading(false)
     }
   }
 
   const addTodo = async () => {
     if (!newTodo.trim()) return
 
     setLoading(true)
     setError(null)
     try {
       const todo = await invoke<Todo>('create_todo', { title: newTodo })
       setTodos([...todos, todo])
       setNewTodo('')
     }
     catch (err) {
       setError(err as TodoError)
     }
     finally {
       setLoading(false)
     }
   }
 
   const toggleTodo = async (id: number, completed: boolean) => {
     setError(null)
     try {
       const updated = await invoke<Todo>('update_todo', {
         id,
         title: null,
         completed: !completed
       })
       setTodos(todos.map(t => t.id === id ? updated : t))
     }
     catch (err) {
       setError(err as TodoError)
     }
   }
 
   const deleteTodo = async (id: number) => {
     setError(null)
     try {
       await invoke('delete_todo', { id })
       setTodos(todos.filter(t => t.id !== id))
     }
     catch (err) {
       setError(err as TodoError)
     }
   }
 
   const renderError = () => {
     if (!error) return null
 
     let message = ''
     switch (error.type) {
       case 'NotFound':
         message = `Todo #${error.details} が見つかりません`
         break
       case 'InvalidTitle':
         message = error.details as string
         break
       case 'Database':
         message = `データベースエラー: ${error.details}`
         break
       case 'Io':
         message = `IOエラー: ${error.details}`
         break
     }
 
     return (
       <div
         style={{
           padding: '12px',
           backgroundColor: '#fee',
           border: '1px solid #f88',
           borderRadius: '4px',
           marginBottom: '16px',
           display: 'flex',
           justifyContent: 'space-between',
           alignItems: 'center'
         }}
       >
         <span style={{ color: '#c00' }}> {message}</span>
         <button
           onClick={() => setError(null)}
           style={{
             background: 'none',
             border: 'none',
             fontSize: '18px',
             cursor: 'pointer'
           }}
         >
           ×
         </button>
       </div>
     )
   }
 
   return (
     <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
       <h1>Todo App</h1>
 
       {renderError()}
 
       <div style={{ marginBottom: '20px' }}>
         <input
           type="text"
           value={newTodo}
           onChange={(e) => setNewTodo(e.target.value)}
           placeholder="新しいTodoを入力"
           style={{
             padding: '8px',
             marginRight: '8px',
             width: '300px',
             fontSize: '16px'
           }}
           onKeyDown={(e) => e.key === 'Enter' && addTodo()}
         />
         <button
           onClick={addTodo}
           disabled={loading || !newTodo.trim()}
           style={{
             padding: '8px 16px',
             fontSize: '16px'
           }}
         >
           追加
         </button>
       </div>
 
       {loading && <p>Loading...</p>}
 
       <ul style={{ listStyle: 'none', padding: 0 }}>
         {todos.map((todo) => (
           <li
             key={todo.id}
             style={{
               display: 'flex',
               alignItems: 'center',
               padding: '12px',
               borderBottom: '1px solid #eee'
             }}
           >
             <input
               type="checkbox"
               checked={todo.completed}
               onChange={() => toggleTodo(todo.id, todo.completed)}
               style={{ marginRight: '12px' }}
             />
             <span
               style={{
                 flex: 1,
                 textDecoration: todo.completed ? 'line-through' : 'none',
                 color: todo.completed ? '#888' : 'inherit'
               }}
             >
               {todo.title}
             </span>
             <button
               onClick={() => deleteTodo(todo.id)}
               style={{
                 color: '#c00',
                 background: 'none',
                 border: '1px solid #c00',
                 padding: '4px 8px',
                 cursor: 'pointer'
               }}
             >
               削除
             </button>
           </li>
         ))}
       </ul>
 
       {todos.length === 0 && !loading && (
         <p style={{ textAlign: 'center', color: '#888' }}>
           Todoがありません
         </p>
       )}
     </div>
   )
 }
 
 export default TodoApp


ログ出力とデバッグ

エラーのログ出力とデバッグの定義例を以下に示す。

 use log::{error, warn, info};
 use env_logger;
 
 #[tauri::command]
 fn debug_operation(input: String) -> Result<String, String> {
    info!("Starting operation with input: {}", input);
 
    if input.is_empty() {
       warn!("Empty input received");
       return Err("Input cannot be empty".to_string())
    }
 
    match risky_operation(&input) {
       Ok(result) => {
          info!("Operation succeeded: {}", result);
          Ok(result)
       }
       Err(e) => {
          error!("Operation failed: {}", e);
          Err(e)
       }
    }
 }
 
 fn risky_operation(input: &str) -> Result<String, String> {
    // 危険な操作
    Ok(format!("Processed: {}", input))
 }


 // main.rsでのロガー初期化
 
 fn main() {
    // ロガーの初期化
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
       .init();
 
    tauri::Builder::default()
       .invoke_handler(tauri::generate_handler![debug_operation])
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }



推奨される事柄

エラーメッセージの設計

ユーザに分かりやすいエラーメッセージを設計する。

  • 具体的な説明
    何が問題だったか、どのように解決できるかを含める。
  • 多言語対応
    必要に応じてエラーメッセージを国際化する。
  • セキュリティ考慮
    内部実装の詳細を漏らさない。


エラーの分類

エラーを適切に分類して処理する。

 #[derive(Debug, Serialize)]
 enum ErrorSeverity {
    Warning,   // ユーザに通知のみ
    Error,     // 操作の再試行が可能
    Critical,  // アプリケーションの再起動が必要
 }
 
 #[derive(Debug, Serialize)]
 struct ClassifiedError {
    severity: ErrorSeverity,
    code: String,
    message: String,
    recoverable: bool,
 }


エラーの伝播

エラーは適切なレベルで処理して、不要な伝播を避ける。

 #[tauri::command]
 fn outer_operation() -> Result<String, String> {
    // 内部エラーはここで変換
    inner_operation().map_err(|e| {
       // ユーザ向けのメッセージに変換
       format!("操作に失敗しました: {}", e)
    })
 }


テスト可能なエラー処理

エラー処理をテスト可能に設計する。

 #[cfg(test)]
 mod tests {
    use super::*;
 
    #[test]
    fn test_create_todo_empty_title() {
       let result = create_todo("".to_string());
       assert!(result.is_err());
       match result {
          Err(TodoError::InvalidTitle(_)) => (),
          _ => panic!("Expected InvalidTitle error"),
       }
    }
 
    #[test]
    fn test_create_todo_valid() {
       let result = create_todo("Test Todo".to_string());
       assert!(result.is_ok());
    }
 }



関連情報