Result 型を使ってエラーハンドリングする

Recipe ID: rust-004

Rust の Result 型を使って、処理の成功/失敗をフロントエンドに伝える方法を解説します。

Tauri のコマンド(#[tauri::command])は、戻り値として Result<T, E> を返すことができます。これにより、Rust 側の処理結果が自動的に JavaScript の Promise に変換されます。

  • 成功時 (Ok(val)): フロントエンドの PromiseResolve (成功) され、val が渡されます。
  • 失敗時 (Err(e)): フロントエンドの PromiseReject (失敗) され、e がエラーとしてキャッチされます。

Rust は例外(Exception)ではなく Result 型でエラーを表現するのが特徴です。Tauri はこの仕組みをうまくフロントエンドの非同期処理(Async/Await)に繋いでくれます。

1. 文字列のエラーメッセージを返す(基本)

最もシンプルな方法は、エラー型(Err の中身)として String を使うことです。
処理に失敗した場合に、エラーメッセージを文字列としてフロントエンドに返します。

Rust (Backend)

Result<f64, String> は、「成功したら f64 (浮動小数点数) を返し、失敗したら String を返す」という意味です。

#[tauri::command]
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        // エラーの場合は Err で包んで返します。
        // リテラル文字列 "&str" を "String" に変換するために .into() を使っています。
        // .to_string() でも同様です。
        return Err("ゼロ除算は許可されていません".into());
    }
    
    // 成功した場合は Ok で包んで返します。
    Ok(a / b)
}

TypeScript (Frontend)

フロントエンド側では try...catch 構文を使ってエラーを補足します。

import { invoke } from '@tauri-apps/api/core';

async function performDivision() {
  try {
    // 成功時はこちら(Okの中身が返る)
    const response = await invoke('divide', { a: 10, b: 0 });
    console.log('計算結果:', response);
  } catch (error) {
    // 失敗時はこちら(Errの中身が返る)
    // error は "ゼロ除算は許可されていません" という文字列になります
    console.error('エラーが発生しました:', error);
    alert(error); 
  }
}

2. カスタムエラー型を返す(応用)

文字列だけでなく、オブジェクト(構造体や Enum)をエラーとして返すこともできます。
これには serde::Serialize トレイトが必要になります。
「データベースエラー」や「入力値エラー」など、エラーの種類をフロントエンド側でプログラム的に判別したい場合に便利です。

Rust (Backend)

エラーの種類を判別するための enum を定義し、Serialize を実装します。

use serde::Serialize;

// フロントエンドに渡すエラー型を定義
// Serialize をつけることで JSON に変換可能になります
#[derive(Debug, Serialize)]
pub enum AppError {
    InvalidInput(String), // 不正な入力(理由付き)
    DatabaseError(String), // DBエラー
    NotFound,             // 見つからない
}

#[tauri::command]
fn get_user(id: i32) -> Result<String, AppError> {
    if id < 0 {
        // 入力が不正な場合
        return Err(AppError::InvalidInput("IDは正の整数である必要があります".into()));
    }

    if id == 99 {
        // データが見つからない場合
        return Err(AppError::NotFound);
    }
    
    // 成功
    Ok(format!("User {}", id))
}

TypeScript (Frontend)

Rust の Enum はデフォルトで特定の形状のオブジェクトにシリアライズされます。

import { invoke } from '@tauri-apps/api/core';

async function fetchUser() {
  try {
    const user = await invoke('get_user', { id: -1 });
    console.log(user);
  } catch (error: any) {
    // error は以下のようなオブジェクトになります
    // { InvalidInput: "IDは正の整数である必要があります" }
    // または
    // "NotFound" (フィールドを持たない Enum バリアントの場合、文字列になることがあります ※設定による)

    if (error.InvalidInput) {
        console.error('入力エラー:', error.InvalidInput);
    } else if (error === 'NotFound' || error.NotFound) {
        console.error('ユーザーが見つかりません');
    } else {
        console.error('その他のエラー:', error);
    }
  }
}

まとめ

  • 単純な通知なら: Result<T, String> を使い、Err("msg".into()) で返します。
  • リッチな制御なら: Serialize を実装した Enum を使います。
  • Promiseの対応: Rust の Ok が JS の Resolve、Err が JS の Reject に対応します。