mcpField
R
rust-errors

rust-errors

github.com/epicenterhq开发4.8k94 次安装2 周前更新
代码仓库

关于 rust-errors

说明 Rust 与 TypeScript 间的错误处理规范,重点涉及 Tauri IPC。内容包括带标签的错误数据、验证、穷尽处理及两种语言间的日志实践对应。

主要功能

  1. 01带标签的 Rust 错误枚举模式
  2. 02TypeScript 运行时验证建议
  3. 03穷尽式错误处理
  4. 04Tauri IPC 绑定约定
  5. 05Rust 与 TypeScript 日志实践对应

适用场景

  1. 01设计 Rust 到 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 代码、生成的绑定、已安装的 crate、TypeScript 类型、源代码或官方文档核实关键细节。

对于下文已说明的本地错误命名约定,无需查询 DeepWiki。

错误的判别联合模式

通过 Tauri 命令将错误从 Rust 传递到 TypeScript 时,使用内部标记枚举来创建 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. 使用内部标记枚举:#[serde(tag = "name")] 会创建判别字段
  2. 遵循命名约定:枚举变体应使用 PascalCase
  3. 包含结构化数据:每个变体都可以包含 message: String 等字段
  4. 单变体枚举也可以:当需要一致的错误结构时使用
// 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 枚举会序列化为以下适用于 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. 尽可能将枚举设为私有:仅在跨模块使用时设为公开

Tauri 命令接口规则

对于会生成 TypeScript 绑定的 Tauri 命令:

  • 当值需要双向跨越 IPC 边界时,派生 Serialize、Deserialize。
  • 对出现在生成绑定中的命令输入、输出和事件载荷派生 specta::Type。
  • Rust 枚举变体名称应与 TypeScript 判别值一致,除非有意使用 #[serde(rename = "...")]。
  • 使用 thiserror 将面向用户的消息字符串放在错误变体上;除非界面需要结构化处理,否则不要让 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?)逐个 token/逐条消息的噪声,用于深度调试
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 日志 crate 会将级别附加到错误类型上(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,并在分支中记录日志。只有当 Result 通过 .then(...) 链从函数中流出时,才使用 tapErr。

评论

支持 Markdown