关于 error-handling
面向 Epicenter 项目的编码技能,指导开发者使用 wellcrafted 处理异常与 `Result` 值,明确错误契约、保留类型化失败,并处理所有可能的错误分支。
主要功能
- 01选择抛出异常或 `Result` 契约
- 02包装同步和异步操作
- 03映射已知失败并重抛未知异常
- 04处理所有可能的 `Err` 分支
- 05明确异步结果的所有权
适用场景
- 01将抛出异常的 API 转换为返回 `Result`
- 02将未知异常保留为程序错误
- 03检查 `Result` 的消费路径
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill error-handling安装后在 Claude Code 中输入/error-handling确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/http-boundaries.mdtoast-on-error.mdwrapping-boundaries.md详细介绍
错误处理
本技能负责定义抛出异常与 Result 值之间的边界,以及正确消费 Result。可与 define-errors(用于变体设计)、logging(用于诊断)、query-layer(用于 RPC 呈现)和 hono(用于响应 API)配合使用。
信息来源
所有关于 Wellcrafted 行为的说明,都应以官方 wellcrafted-dev/wellcrafted 源码和测试为依据。维护本指南时,请确认 Epicenter 安装的版本与所查阅的源码版本一致。如果不一致,请报告版本差异;依赖项更新由本技能之外的流程负责。其他技能、示例、生成的文档和 DeepWiki 只能作为线索,不能作为权威依据。
仅在需要时阅读对应的参考资料:
- 决定单个
trySync或tryAsync应覆盖多少工作时,尤其是在处理清理逻辑时,请阅读 references/wrapping-boundaries.md。 - 在 UI 代码中呈现带标签的失败时,请阅读 references/toast-on-error.md。
- 将失败映射到 Hono 响应,或决定哪些异常必须继续向上传播时,请阅读 references/http-boundaries.md。
先选择契约
| 所需契约 | 模式 |
|---|---|
调用方接收 Result<T, E> | 使用 trySync 或 tryAsync 转换会抛出异常的操作 |
| 失败时有有效的回退值 | 从 catch 返回 Ok(fallback) |
| 失败必须作为数据传播 | 从 catch 返回类型化的 defineErrors 工厂结果 |
只有已知的外部失败应转换为 Err | 映射已知异常,并重新抛出未知异常 |
| 外层 API 基于异常 | 保留其抛出异常的契约 |
| 无论成功、失败、取消还是提前返回,都必须执行清理 | 使用 finally |
同步操作使用 trySync,返回 Promise 的操作使用 tryAsync。
const { data: response, error } = await tryAsync({
try: () => fetch(url),
catch: (cause) => RequestError.TransportFailed({ cause }),
});
if (error !== null) return Err(error);
return Ok(response);
defineErrors 工厂已会返回 Err(...)。将原始 cause 传给工厂,并由工厂通过 extractErrorMessage 组合错误消息。不要在 catch 边界使用原始的 Err(cause):抛出的值可能是 null 或 undefined,而未类型化的原因会丢失领域层面的失败信息。
跨越明确的抛出边界
只要调用方仍可恢复、报告、重试或传播失败,就将 Result 保留为数据。仅在外层 API 已将抛出异常作为失败通道时才使用 unwrap:
import { unwrap } from 'wellcrafted/result';
const document = unwrap(await openDocument(id));
unwrap 会返回 Ok.data,并抛出 Err.error。抛出异常是该边界的契约,并不表示失败出乎意料。如果此函数需要恢复、补充上下文或执行清理,请改用 error !== null 进行判断。
处理所有可能的 Err 分支
- 如果一个值可能是
Result<T, E>,就检查其错误分支,或有意将其向外传递。 - 解构后,
error是原始的E。应返回Err(error),而不是error。 - 如果保留了整个 Result,就原样返回:
if (result.error !== null) return result。 error !== null是可靠的判别方式。绝不要构造Err(null)或Err(undefined)。- 如果
catch分支总是返回Ok<T>,且推断类型会收窄为Ok<T>,则只解构数据字段是正确的。 - 如果成功数据无关紧要,则只解构错误字段是正确的。原则是处理所有可能出现的
Err,而不是解构未使用的字段。
优先使用立即执行的守卫,让成功路径保持线性。
拥有 Promise
tryAsync 会返回一个 Promise。请明确指定其归属方:
- 当此函数检查 Result 时,使用
await。 - 当调用方拥有
Promise<Result<...>>时,使用return tryAsync(...)。 - 不要直接使用
void tryAsync(...):普通失败会以Err的形式正常完成,因此 Promise 拒绝处理器无法捕获它们。尽力而为的操作仍需要由异步归属方负责:等待 Result,并明确记录或忽略其错误分支。 - 在 UI 的即发即弃代码中,丢弃以 Result 正常完成的 Promise 之前,先附加呈现逻辑:
void save().then((result) => toastOnError(result, 'Save failed'))。如果 Promise 可能拒绝,请先转换或捕获该拒绝。
当原生 Try-Catch 能表达约定时,请保留它
以下情况适合使用传统的 try-catch:
finally代码块负责清理;- 生成器必须
yield一个失败,而不是返回 Result; - 框架边界会将异常直接转换为所需的响应格式;
- 代码捕获一种已知异常,并重新抛出其他所有异常;
- 外围公共 API 有意抛出异常。
不要将未知的编程错误转换为通用的领域失败。将所有抛出都映射为 Err,可能会用具有误导性的可重试错误掩盖 Bug。
最终检查
- 函数抛出异常还是返回 Result,约定是否明确。
- 包装器是否涵盖一种连贯的失败含义。
- 捕获的值是否转换为类型化错误、有意采用的回退方案,或选择性地重新抛出。
- 每个可能出现的
Err分支是否都由明确命名的尽力而为归属方处理、转发、记录、呈现或明确忽略。 - 未知 Bug 是否仍会到达适当的崩溃边界或框架错误边界。