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 側のログに残し、フロントには要約だけ返すと扱いやすくなります。ダイアログで通知するなら エラー発生時に警告ダイアログを出す を組み合わせます。
