关于 rust-errors
说明 Rust 与 TypeScript 间的错误处理规范,重点涉及 Tauri IPC。内容包括带标签的错误数据、验证、穷尽处理及两种语言间的日志实践对应。
主要功能
- 01带标签的 Rust 错误枚举模式
- 02TypeScript 运行时验证建议
- 03穷尽式错误处理
- 04Tauri IPC 绑定约定
- 05Rust 与 TypeScript 日志实践对应
适用场景
- 01设计 Rust 到 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 代码、生成的绑定、已安装的 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 的关键模式
- 使用内部标记枚举:
#[serde(tag = "name")]会创建判别字段 - 遵循命名约定:枚举变体应使用 PascalCase
- 包含结构化数据:每个变体都可以包含
message: String等字段 - 单变体枚举也可以:当需要一致的错误结构时使用
// 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" }
最佳实践
- 错误结构保持一致:所有错误都具有包含
name和message的相同结构 - TypeScript 类型安全:使用 arktype 进行运行时验证,以确保类型安全
- 穷尽处理:
switch语句可在编译时检查是否已穷尽所有情况 - 不要使用
content属性:避免使用#[serde(tag = "name", content = "data")],因为它会创建嵌套结构 - 尽可能将枚举设为私有:仅在跨模块使用时设为公开
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。