mcpField
R
rust-errors

rust-errors

github.com/epicenterhq開発4.8k94 回インストール2 週間前に更新
リポジトリ

rust-errors について

Rust と TypeScript 間のエラー処理規約を説明するスキルで、特に Tauri IPC を扱います。タグ付きエラーデータ、検証、網羅的な処理、両言語のログ運用をまとめています。

主な機能

  1. 01タグ付き Rust エラー enum のパターン
  2. 02TypeScript の実行時検証の指針
  3. 03網羅的なエラー処理
  4. 04Tauri IPC バインディングの規約
  5. 05Rust と TypeScript のログ対応

活用シーン

  1. 01Rust から TypeScript へ渡す IPC エラーの設計
  2. 02シリアライズ済みエラーペイロードの検証
  3. 03言語間で一貫したエラーログの適用

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/epicenterhq/epicenter --skill rust-errors

インストール後、Claude Code で/rust-errorsを入力して読み込まれたか確認します。

詳細

Rust から TypeScript へのエラー処理

参照リポジトリ

  • Tauri:デスクトップアプリのフレームワーク(Rust から TypeScript へのエラーパターンの参照元)

上流の情報による根拠確認

Rust のエラーのシリアライズ、Tauri コマンドのエラー転送、IPC ペイロードの形式、生成されたバインディング、またはフロントエンドの invoke エラーの動作が正確性に影響する場合は、記憶だけに頼る前にソースコードに基づいて確認してください。DeepWiki MCP が利用可能な場合は、tauri-apps/tauri に対して範囲を絞った質問をしてください。利用できない場合や、リポジトリがインデックス化されていない場合は、上流のソースコードまたは公式ドキュメントを直接参照してください。DeepWiki は概要を把握するために使い、重要な詳細については、コードを変更する前にローカルの Rust コード、生成されたバインディング、インストール済みのクレート、TypeScript の型、ソースコード、または公式ドキュメントで検証してください。

以下に記載されているローカルのエラー命名規則については、DeepWiki を使う必要はありません。

エラーの識別可能なユニオンパターン

Tauri コマンドを介して Rust から TypeScript にエラーを渡す場合は、内部タグ付き enum を使用して、TypeScript で自然に扱える識別可能なユニオンを作成してください。

Rust のエラー定義
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Error, Debug, Serialize, Deserialize)]
#[serde(tag = "name")]
pub enum TranscriptionError {
    #[error("Audio read error: {message}")]
    AudioReadError { message: String },

    #[error("GPU error: {message}")]
    GpuError { message: String },

    #[error("Model load error: {message}")]
    ModelLoadError { message: String },

    #[error("Transcription error: {message}")]
    TranscriptionError { message: String },
}
Rust の主なパターン
  1. 内部タグ付き enum を使用する:#[serde(tag = "name")] によって判別フィールドが作成されます
  2. 命名規則に従う:enum のバリアント名には PascalCase を使用してください
  3. 構造化データを含める:各バリアントには message: String などのフィールドを含められます
  4. バリアントが 1 つだけの enum も問題ありません:一貫したエラー構造が必要な場合に使用してください
// Single-variant enum for consistency
#[derive(Error, Debug, Serialize, Deserialize)]
#[serde(tag = "name")]
enum ArchiveExtractionError {
    #[error("Archive extraction failed: {message}")]
    ArchiveExtractionError { message: String },
}
TypeScript のエラー処理
import { type } from 'arktype';

// Define the error type to match Rust serialization
const TranscriptionErrorType = type({
	name: "'AudioReadError' | 'GpuError' | 'ModelLoadError' | 'TranscriptionError'",
	message: 'string',
});

// Use in error handling
const result = await tryAsync({
	try: () => invoke('transcribe_audio_whisper', params),
	catch: (unknownError) => {
		const result = TranscriptionErrorType(unknownError);
		if (result instanceof type.errors) {
			// Handle unexpected error shape
			return WhisperingErr({
				title: 'Unexpected Error',
				description: extractErrorMessage(unknownError),
				action: { type: 'more-details', error: unknownError },
			});
		}

		const error = result;
		// Now we have properly typed discriminated union
		switch (error.name) {
			case 'ModelLoadError':
				return WhisperingErr({
					title: 'Model Loading Error',
					description: error.message,
					action: {
						type: 'more-details',
						error: new Error(error.message),
					},
				});

			case 'GpuError':
				return WhisperingErr({
					title: 'GPU Error',
					description: error.message,
					action: {
						type: 'link',
						label: 'Configure settings',
						href: '/settings/transcription',
					},
				});

			// Handle other cases...
		}
	},
});
シリアライズ形式

Rust の enum は、TypeScript で扱いやすい次の形式にシリアライズされます。

// AudioReadError variant
{ "name": "AudioReadError", "message": "Failed to decode audio file" }

// GpuError variant
{ "name": "GpuError", "message": "GPU acceleration failed" }
ベストプラクティス
  1. 一貫したエラー構造:すべてのエラーが name と message を持つ同じ形式になります
  2. TypeScript の型安全性:実行時検証に arktype を使用して、型安全性を確保してください
  3. 網羅的な処理:switch 文により、コンパイル時に網羅性をチェックできます
  4. content 属性を使わない:ネストした構造が作られるため、#[serde(tag = "name", content = "data")] は避けてください
  5. 可能な限り enum を非公開にする:モジュール間で使用する場合にのみ公開してください

Tauri コマンドの公開インターフェース規則

TypeScript バインディングを生成する Tauri コマンドでは、次のようにしてください。

  • 値が IPC 境界を双方向に越える場合は、Serialize と Deserialize を導出します。
  • 生成されたバインディングに含まれるコマンドの入力、出力、イベントペイロードには、specta::Type を導出します。
  • 意図的に #[serde(rename = "...")] を指定する場合を除き、Rust の enum バリアント名を TypeScript の判別子と一致させます。
  • ユーザーに表示するメッセージ文字列は、thiserror を使ってエラーバリアントに保持します。UI で構造化された処理が必要でない限り、TypeScript 側で別々のフィールドから Rust のコンテキストを再構築させないでください。
  • イベントの型がコマンドから返されない場合でも、Tauri specta ビルダーにイベントを登録します。

生成されたバインディングは、単なる出力ではなく、契約の確認手段です。Rust の変更によって TypeScript のコマンドまたはイベントの公開インターフェースが変わる場合は、バインディングを再生成し、生成された差分を確認してください。生成された差分が大きい一方で、公開される IPC の形式が変わっていない場合は、そのままコミットせず、原因を調べてください。

避けるべきアンチパターン
// DON'T: External tagging (default behavior)
#[derive(Serialize)]
pub enum BadError {
    ModelLoadError { message: String }
}
// Produces: { "ModelLoadError": { "message": "..." } }

// DON'T: Adjacent tagging with content
#[derive(Serialize)]
#[serde(tag = "type", content = "data")]
pub enum BadError {
    ModelLoadError { message: String }
}
// Produces: { "type": "ModelLoadError", "data": { "message": "..." } }

// DON'T: Manual Serialize implementation when derive works
impl Serialize for MyError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        // Unnecessary complexity
    }
}

このパターンを使うと、ボイラープレートを最小限に抑えながら、Rust と TypeScript の境界を越えたクリーンで型安全なエラー処理を実現できます。

tracing ↔ wellcrafted/logger

defineErrors は thiserror を、ワークスペースのロガーは tracing をそれぞれ反映しています。これらを組み合わせることで、TypeScript でも Rust と同じように、エラーをデータとして扱い、出力箇所でレベルを選択できます。

レベルのマッピング(5 レベル、fatal なし)
tracing マクロワークスペースの Logger メソッド使用する場面
tracing::trace!(...)log.trace(message, data?)詳細なデバッグのためのトークン単位/メッセージ単位のノイズ
tracing::debug!(...)log.debug(message, data?)内部状態の遷移(ハンドシェイク、キャッシュの充填)
tracing::info!(...)log.info(message, data?)ライフサイクルイベント(接続、読み込み、フラッシュ)
tracing::warn!(?err)log.warn(err)回復可能な失敗:リトライ経路、フォールバックの採用
tracing::error!(?err)log.error(err)このレイヤーでは回復不能:大声で記録する

tracing に fatal はありません。私たちにもありません。プロセスの終了はアプリの判断(process.exit)であり、ライブラリの判断ではありません。

バリアントにレベルを持たせる? いいえ。
// Rust: level is on the CALL, not the enum variant
tracing::warn!(?err, "cache miss"); // same err, different sites
tracing::error!(?err, "giving up");
// TS: same rule
log.warn(CacheError.Miss({ key }));  // recoverable
log.error(CacheError.Miss({ key })); // terminal

レベルをエラー型に付与する Rust のロギングクレートはありません(thiserror、anyhow、slog、log)。例外は miette ですが、それはコンパイラ診断ライブラリであり、汎用ロガーではありません。私たちは tracing に従います。レベルはコンテキストであり、識別情報ではありません。

?err イディオム ↔ tapErr

tracing の ?err は、構造化されたエラーフィールドをログイベントに補間します。TS では、Result フローにおける同等のものは tapErr です(wellcrafted/result より):

let result = do_thing().inspect_err(|err| tracing::warn!(?err, "do_thing failed"));
import { tapErr } from 'wellcrafted/result';

const result = await tryAsync({
  try: () => doThing(),
  catch: (cause) => DoThingError.Failed({ cause }),
}).then(tapErr(log.warn));

どちらも、成功時はパススルーし、失敗時は構造化されたエラーをログに記録します。

実際には、この形は epicenter ではまれです。ほとんどの呼び出し箇所では Ok のデータをローカルで必要とするため、result.error で分岐し、その分岐内でログを記録します。tapErr を使うのは、Result が .then(...) チェーンで関数の外へ流れ出る場合だけです。

コメント