Rust 側で起きたエラーを JS でキャッチする

Rust コマンドが Err を返すと invoke の Promise が reject される。try/catch での受け方と、独自エラー型や thiserror で JS に届くエラーの形がどう変わるかを示す。

フロントエンド 対象: Tauri 2.x 更新日: 読了目安: 約6分 front-004
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 2. バックエンドから実装する (Rust)
  4. パターン A: Result<T, String>(文字列で届く)
  5. パターン B: 独自エラー型 + serde::Serialize(オブジェクトで届く)
  6. パターン C: thiserror + 手動 Serialize(文字列で届く)
  7. 動作確認
  8. よくあるエラーと対処法
  9. the trait bound MyError: serde::Serialize is not satisfied という趣旨のコンパイルエラー
  10. 画面に [object Object] と表示される
  11. catch に来ない、または何も起きない
  12. OS ごとの違いと注意点
  13. 関連レシピ

Rust コマンドの戻り値を Result<T, E> にすると、Ok(v) は invoke の resolve に、Err(e) は reject に対応します。フロントエンドでは try / catch で受けるだけですが、catch に届く値の 形 は Rust 側のエラー型によって「文字列」にも「オブジェクト」にもなります。Rust 側の詳しい書き方は Result 型を使ってエラーハンドリングする を参照してください。

前提条件

追加プラグインや権限設定は不要です。thiserror を使う場合は src-tauri で cargo add thiserror を実行します。

1. フロントエンドから実装する (TypeScript)

catch で受け取る値は unknown として扱い、文字列かオブジェクトかを判定してからメッセージを組み立てます。

// src/main.ts
import { invoke } from '@tauri-apps/api/core';

// Rust の enum AppError(後述)に対応する型
interface AppError {
  kind: 'NotFound' | 'Io' | 'Validation';
  message: string;
}

function isAppError(e: unknown): e is AppError {
  return typeof e === 'object' && e !== null && 'kind' in e && 'message' in e;
}

async function divide(a: number, b: number) {
  try {
    console.log('結果:', await invoke<number>('divide', { a, b }));
  } catch (error) {
    console.error('計算エラー:', error); // Result<f64, String> なので string
  }
}

async function openConfig(path: string) {
  try {
    return await invoke<string>('read_config', { path });
  } catch (error) {
    if (isAppError(error)) {
      // Result<String, AppError> なのでオブジェクト
      if (error.kind === 'NotFound') alert(`ファイルがありません: ${error.message}`);
      else alert(`読み込みに失敗しました: ${error.message}`);
    } else {
      alert(String(error)); // コマンド未登録など Tauri 自身のエラー(文字列)
    }
    return null;
  }
}

error は JS の Error インスタンス ではない ので、error.message や error.stack は独自エラー型でそう設計した場合にしか存在しません。

2. バックエンドから実装する (Rust)

パターン A: Result<T, String>(文字列で届く)

一番簡単な形です。標準ライブラリのエラーは map_err(|e| e.to_string()) で変換します。

#[tauri::command]
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        return Err("0 で割ることはできません".into());
    }
    Ok(a / b)
}

パターン B: 独自エラー型 + serde::Serialize(オブジェクトで届く)

enum に Serialize を derive すると catch にオブジェクトが届きます。既定の外部タグ形式({ "NotFound": "..." })は JS で扱いにくいので、#[serde(tag = "kind", content = "message")] で { kind, message } の形に揃えます。

// src-tauri/src/lib.rs
use serde::Serialize;

#[derive(Debug, Serialize)]
#[serde(tag = "kind", content = "message")]
enum AppError {
    NotFound(String),
    Io(String),
    Validation(String),
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        match e.kind() {
            std::io::ErrorKind::NotFound => AppError::NotFound(e.to_string()),
            _ => AppError::Io(e.to_string()),
        }
    }
}

#[tauri::command]
fn read_config(path: String) -> Result<String, AppError> {
    let text = std::fs::read_to_string(&path)?; // io::Error -> AppError に自動変換
    if !text.contains("[app]") {
        return Err(AppError::Validation("[app] セクションがありません".into()));
    }
    Ok(text)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![divide, read_config])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

パターン C: thiserror + 手動 Serialize(文字列で届く)

公式ドキュメントの例はこの形です。#[from] で ? が使え、Serialize を serialize_str で実装しているため JS には Display の文字列が届きます。

#[derive(Debug, thiserror::Error)]
enum Error {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error("設定が不正です: {0}")]
    Validation(String),
}

impl serde::Serialize for Error {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(&self.to_string())
    }
}

動作確認

npm run tauri dev で起動し、コンソールから呼び出します。

  • divide(10, 0) → 計算エラー: 0 で割ることはできません(文字列)。
  • openConfig('C:/nothing.toml') → catch に { kind: "NotFound", message: "... (os error 2)" } のようなオブジェクトが届き、alert が出ます。

よくあるエラーと対処法

the trait bound MyError: serde::Serialize is not satisfied という趣旨のコンパイルエラー

Result<T, E> の E にも Serialize が必要です。anyhow::Error や std::io::Error は直接返せません。map_err(|e| e.to_string()) で String にするか、パターン B / C の独自型を用意します。

画面に [object Object] と表示される

エラーがオブジェクトで届いているのに String(error) で文字列化しています。error.message を取り出すか、JSON.stringify(error) でデバッグ表示してください。逆に error.message が undefined なら、エラーは文字列で届いています。

catch に来ない、または何も起きない

Rust 側が Option や bool で失敗を表現していると reject にはなりません。また unwrap() の失敗(panic)は Err ではないので catch では捕まえられず、アプリの異常終了につながります。unwrap() は ? や map_err に置き換えてください(パニック(クラッシュ)時の処理を書く)。

OS ごとの違いと注意点

エラーの伝達方法に OS 差はありませんが、メッセージの 内容 は OS ごとに変わります。

  • std::io::Error の to_string() は OS のロケールで翻訳されます(Windows の日本語環境では「指定されたファイルが見つかりません。 (os error 2)」、Linux では「No such file or directory (os error 2)」)。メッセージの文字列で分岐せず、ErrorKind や独自の kind フィールドで分岐してください。
  • 詳細は Rust 側のログに残し、フロントには要約だけ返すと扱いやすくなります。ダイアログで通知するなら エラー発生時に警告ダイアログを出す を組み合わせます。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する