パニック(クラッシュ)時の処理を書く

Recipe ID: rust-020

Rust は安全な言語ですが、unwrap() の失敗などで「パニック(Panic)」が発生し、アプリが突然終了してしまうことがあります。
デフォルトではコンソールに英語のエラーが出るだけでウィンドウが消えてしまうため、ユーザーは「何が起きたのか分からない」状態になります。

これを防ぐために、パニック発生時に「エラーダイアログを出す」「ログファイルに書き出す」といったフック処理を設定できます。

1. ダイアログ表示用クレートの追加

パニック時は Tauri の機能自体も不安定になっている可能性があるため、OS のネイティブダイアログを直接呼べる小さなライブラリ rfd (Rust File Dialog) が便利です。

ターミナルで src-tauri ディレクトリに移動し、以下のコマンドを実行して rfd クレートを追加します。

cd src-tauri
cargo add rfd

2. 実装と動作確認

Tauri v2 の標準構成である src-tauri/src/lib.rs に、以下のコードを実装します。ここではパニックフックの設定に加え、動作確認用に意図的にパニックを起こすコマンド (panic_app) も追加しています。

Rust 側 (src-tauri/src/lib.rs)

run 関数でフックを設定し、テスト用コマンドを登録します。

// src-tauri/src/lib.rs
use std::fs::File;
use std::io::Write;
use std::panic;

// 動作確認用のコマンド:意図的にパニックを起こす
#[tauri::command]
fn panic_app() {
    panic!("これはテスト用のパニックです!");
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    // 独自のパニックフックを設定
    panic::set_hook(Box::new(|info| {
        // 1. エラーメッセージの取り出し
        let msg = match info.payload().downcast_ref::<&'static str>() {
            Some(s) => *s,
            None => match info.payload().downcast_ref::<String>() {
                Some(s) => &s[..],
                None => "Unknown error",
            },
        };

        // 2. 発生場所(ファイル名・行番号)の取り出し
        let location = match info.location() {
            Some(l) => format!("File: {}, Line: {}", l.file(), l.line()),
            None => "Unknown location".to_string(),
        };

        let report = format!("申し訳ありません、エラーが発生しました。\n\n内容: {}\n場所: {}", msg, location);

        // 3. 標準エラー出力にも出す(開発用)
        eprintln!("{}", report);

        // 4. ファイルにログ出力(任意)
        // カレントディレクトリに保存(実際のアプリでは保存先を適切に設定してください)
        if let Ok(mut file) = File::create("crash-report.txt") {
            let _ = writeln!(file, "{}", report);
        }

        // 5. ユーザーへの通知(ダイアログ)
        // MessageDialog は同期処理なのでフック内で安全に使えます
        // 注意: macOSなど一部のOSではメインスレッド以外からのGUI呼び出しが制限される場合があります
        rfd::MessageDialog::new()
            .set_title("Crash Report")
            .set_description(&report)
            .set_level(rfd::MessageLevel::Error)
            .show();
    }));

    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![panic_app]) // テスト用コマンドを登録
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

フロントエンド側 (src/main.ts)

ボタンを押すとバックエンドの panic_app を呼び出す処理を書きます。
index.html<button id="panic-btn">クラッシュさせる</button> がある想定です。

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

// HTML要素の取得とイベントリスナーの登録
const panicBtn = document.querySelector("#panic-btn");

if (panicBtn) {
  panicBtn.addEventListener("click", () => {
    invoke("panic_app");
  });
}

このボタンを押すと、Rust 側で panic! が発生し、設定した rfd のダイアログが表示されるはずです。通常の開発中(デバッグビルド)では、コンソールにも詳細なバックトレースが表示されます。

解説

  • panic::set_hook: Rust ランタイムがパニックした瞬間に呼び出す関数を登録します。ここで設定したクロージャが、パニック発生時に実行されます。
  • info: パニックに関する詳細情報(エラーメッセージや発生したファイル・行番号)が入っています。
  • rfd::MessageDialog: OS ネイティブのダイアログを表示します。Tauri のメインウィンドウやイベントループがクラッシュしていても、OS の機能を使ってエラーを通知できるため有用です。