概要
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におけるエラー処理の概念を示す。
| 型 | 説明 | 使用場面 |
|---|---|---|
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());
}
}
関連情報
- Tauriの基礎 - Commands
- Commandsの基本的な使用方法について
- Tauriの基礎 - 非同期Commands
- 非同期処理、Channel API、プログレス表示について
- Tauri公式ドキュメント - Commands
- 公式のCommandsリファレンス
- thiserrorクレート
- カスタムエラー型の定義用クレート
- anyhowクレート
- 汎用エラーハンドリング用クレート
- Serde公式ドキュメント
- シリアライズ/デシリアライズの詳細