rust-errors について
Rust と TypeScript 間のエラー処理規約を説明するスキルで、特に Tauri IPC を扱います。タグ付きエラーデータ、検証、網羅的な処理、両言語のログ運用をまとめています。
主な機能
- 01タグ付き Rust エラー enum のパターン
- 02TypeScript の実行時検証の指針
- 03網羅的なエラー処理
- 04Tauri IPC バインディングの規約
- 05Rust と TypeScript のログ対応
活用シーン
- 01Rust から TypeScript へ渡す IPC エラーの設計
- 02シリアライズ済みエラーペイロードの検証
- 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 の主なパターン
- 内部タグ付き enum を使用する:
#[serde(tag = "name")]によって判別フィールドが作成されます - 命名規則に従う:enum のバリアント名には PascalCase を使用してください
- 構造化データを含める:各バリアントには
message: Stringなどのフィールドを含められます - バリアントが 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" }
ベストプラクティス
- 一貫したエラー構造:すべてのエラーが
nameとmessageを持つ同じ形式になります - TypeScript の型安全性:実行時検証に arktype を使用して、型安全性を確保してください
- 網羅的な処理:switch 文により、コンパイル時に網羅性をチェックできます
content属性を使わない:ネストした構造が作られるため、#[serde(tag = "name", content = "data")]は避けてください- 可能な限り 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(...) チェーンで関数の外へ流れ出る場合だけです。