关于 web-error-handling-result-types
面向 TypeScript 的技能,使用带判别字段的 Result 类型表示预期失败,避免错误仅隐藏在异常流程中。提供类型化错误、同步与异步组合及合并结果的模式和参考资料。
主要功能
- 01定义 Result 类型与构造函数
- 02用 map 和 flatMap 组合操作
- 03用 tryCatch 转换可能抛错的调用
- 04通过判别联合建模错误
- 05合并结果,支持快速失败或收集全部错误
适用场景
- 01表示验证与解析失败
- 02串联请求或其他可能失败的步骤
- 03收集表单中的多项验证错误
- 04在 API 边界处理类型化失败
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-error-handling-result-types安装后在 Claude Code 中输入/web-error-handling-result-types确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/async.mdcombining.mdcore.md详细介绍
TypeScript Result 类型模式
快速指南:
Result<T, E>是基于ok的判别联合类型,因此在调用方检查之前,TypeScript 会拒绝读取value。这会将函数的失败模式纳入其签名,而异常会隐藏这些失败模式。对于预期内的失败——验证、解析、请求——请使用它;对于 bug 以及下游无法采取任何措施的情况,则保留异常。自定义实现大约只需 40 行,是推荐的默认方案;本技能涵盖了完整的接口。
详细资源:
- examples/core.md — Result 模块、类型化错误定义、封装会抛出异常的代码、模式匹配
- examples/async.md —
Promise<Result>、异步链式调用、重试、转换 promise - examples/combining.md — 快速失败、收集全部错误、对象和顺序组合
- reference.md — 操作查找、Result 不会捕获的内容、错误类型模板
适用路径
- 尚无任何实现 — 编写模块:联合类型、
ok、err、map、flatMap、match、tryCatch。examples/core.md 包含完整文件。 - 类型由库提供 — 操作名称可能不同,但组合方式相同; reference.md 列出了名称对应关系。
- 失败操作是异步的 — 类型为
Promise<Result<T, E>>,并由调用方负责等待; 请参阅 examples/async.md。
<critical_requirements>
编写 Result 代码前
读取 value 或 error 前,先检查 result.ok。 联合类型只有经过该检查才会收窄,因此在进行检查前,TypeScript 会拒绝访问这两个属性;绕过检查会导致运行时得到 undefined。
在返回 Result 的函数中,将每个会抛出异常的调用都封装在 tryCatch 中。 JSON.parse 及类似函数会抛出异常,使其越过返回类型;因此,只要有一个调用未封装,签名就会变成谎言,调用方的穷尽处理也会变得不完整。
为每个错误提供一个判别字段——code 或 type,而不是将其类型设为 Error 或 string。 判别字段可让调用方使用 switch,并让 TypeScript 检查分支是否穷尽;只有消息的错误只能用于显示。
每一步都返回 Result 时,使用 flatMap 链式调用。 错误类型会自动合并,第一次失败会短路后续步骤;而嵌套的 if (result.ok) 代码块其实只是在手动重复这种行为。
处理收到的每一个 Result。 丢弃 Result,就等于让其失败对程序其余部分而言从未发生,而且不会有类型错误提醒你。
</critical_requirements>
自动检测: Result 类型、Either 类型、ok err、铁路导向编程、将错误作为值、flatMap andThen、tryCatch、unwrapOr、combineWithAllErrors、判别联合错误、类型化错误
适用场景:
- 预期内、可恢复的失败——验证、解析、请求、业务规则
- 必须在函数签名中列出所有失败方式的场景
- 链接可能失败的步骤,使首次失败跳过其余步骤
- 一次收集所有失败,例如表单验证所需的场景
其他部分负责:
- 渲染阶段的失败——组件抛出的异常会被其外层的包装逻辑捕获,Result 不会进入该路径。
- 传输和缓存——Result 描述请求的结果;发起请求、重试和缓存由负责获取数据的部分处理。
- 模式验证——报告问题的验证器有其自身的结果格式;在边界处对其进行封装,并将其报告作为错误载荷传递。
- 将失败转换为响应——错误对应的状态码由 API 层决定;本技能只保证错误以类型化的形式传递过来。
## 理念
异常是不可见的控制流:它不会在类型中留下任何痕迹,因此要知道一个函数会抛出异常,唯一的办法就是阅读它,或者在生产环境中措手不及。Result 会将相同的信息放进函数签名,让编译器强制执行。
代价确实存在——每个调用方都必须处理或传递错误,而且随着调用链变长,错误联合类型也会不断扩大。这就是边界如此重要的原因:在入口处将抛出异常的代码转换为 Result,在出口处再将 Result 转换为外部所需的形式。在两者之间,不会抛出异常。
**铁路模式:**成功时沿主线运行;一旦出现第一个错误,就切换到并行线,后续每一步都会跳过,直到某处显式处理该错误。
parseNumber validatePositive double
OK ─────────────────────────────────────────────> success
↘ ↘
ERR ────────────────────────────> failure
<decision_framework>
选择 Result、异常还是可空值
Can the caller do something about this failure?
├─ NO — it is a bug or a condition nothing can act on → throw
│ ├─ Index out of bounds, invalid internal state
│ └─ Missing startup configuration, unreachable database at boot
└─ YES → What does the failure need to carry?
├─ Nothing but its own absence → T | null
├─ A reason the caller branches on → Result<T, E>
└─ Several distinct reasons → Result<T, E> with a discriminated E
如果 Result<User, NotFoundError> 的错误只携带 code: "NOT_FOUND",那它不过是穿着伪装的可空值。只有当调用方接下来要采取的行动因错误原因而异时,才应选择 Result。
**快速失败还是收集所有错误:**表单中有一个字段无效,并不意味着应该隐藏其他四个字段的问题,因此表单验证会收集错误;而如果调用链中的第二步要使用第一步的输出,就没有可收集的内容,只能短路返回。
返回一个值的代价也远低于抛出一个错误,因为抛出的错误会捕获堆栈跟踪并展开调用栈;reference.md 提供了实测对比。这只是热路径上的决胜因素,本身并不足以成为选择它的理由。
</decision_framework>
核心模式
模式 1:类型和构造函数
以 ok 作为判别字段,始终使用 readonly,并在另一侧使用 never,以保持类型推断清晰。
export type Result<T, E = Error> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E };
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
完整代码:examples/core.md
模式 2:map 和 mapError
它们各自转换一侧,并原样传递另一侧,因此可以安全地应用于未经检查的 Result。
export const map = <T, U, E>(
result: Result<T, E>,
fn: (value: T) => U,
): Result<U, E> => (result.ok ? ok(fn(result.value)) : result);
export const mapError = <T, E, F>(
result: Result<T, E>,
fn: (error: E) => F,
): Result<T, F> => (result.ok ? result : err(fn(result.error)));
mapError 是添加上下文的地方——失败的操作,以及导致失败的输入。
模式 3:使用 flatMap 链式调用
每个步骤都会返回自己的 Result,因此错误类型会合并为联合类型,而第一次失败会终止调用链。
export const flatMap = <T, U, E, F>(
result: Result<T, E>,
fn: (value: T) => Result<U, F>,
): Result<U, E | F> => (result.ok ? fn(result.value) : result);
const parsed = flatMap(parseNumber(input), validatePositive);
// Result<number, ParseError | ValidationError>
完整代码:examples/core.md
模式 4:在边界处使用 tryCatch
在抛出异常的代码进入系统时将其转换,并在同一次调用中将错误映射为此领域的类型。
export const tryCatch = <T, E>(
fn: () => T,
onError: (error: unknown) => E,
): Result<T, E> => {
try {
return ok(fn());
} catch (error) {
return err(onError(error));
}
};
const parsed = tryCatch(
() => JSON.parse(json) as Config,
(error): ParseError => ({
code: "PARSE_ERROR",
message: String(error),
input: json,
}),
);
在返回 Result 的函数中,未包装的 JSON.parse 是最常见的导致函数签名名不副实的原因。
完整代码:examples/core.md
模式 5:使用 match 穷尽处理
在一个表达式中处理两种情况,因此它自然适合用作出口边界的转换器。
export const match = <T, E, U>(
result: Result<T, E>,
handlers: { ok: (value: T) => U; err: (error: E) => U },
): U => (result.ok ? handlers.ok(result.value) : handlers.err(result.error));
const response = match(loadUser(id), {
ok: (user) => ({ status: 200, body: user }),
err: (error) => toHttpResponse(error),
});
完整代码:examples/core.md
模式 6:判别式错误联合类型
每个变体都携带其处理器所需的信息,而联合类型则列出函数失败的所有方式。
type UserError =
| { readonly code: "NOT_FOUND"; readonly userId: string }
| {
readonly code: "VALIDATION_ERROR";
readonly field: string;
readonly message: string;
}
| { readonly code: "NETWORK_ERROR"; readonly statusCode: number };
if (!result.ok) {
switch (result.error.code) {
case "NOT_FOUND":
return showMissing(result.error.userId);
case "VALIDATION_ERROR":
return highlightField(result.error.field);
case "NETWORK_ERROR":
return offerRetry();
}
}
添加一个变体会让每个未处理它的 switch 都标红,这正是使用判别字段的全部价值。
完整代码:examples/core.md
模式 7:组合多个 Result
快速失败会返回第一个错误;收集全部错误则会返回所有错误。
export const combine = <T, E>(results: Result<T, E>[]): Result<T[], E> => {
const values: T[] = [];
for (const result of results) {
if (!result.ok) return result;
values.push(result.value);
}
return ok(values);
};
<red_flags>
危险信号
运行时会出错:
- 未检查
ok就读取result.value——使用时值为undefined,而非空断言或类型转换正是让代码通过编译器检查的手段。 - 在返回 Result 的函数中,直接调用可能抛出异常的函数而未包装——异常会逃逸到一个被告知无需捕获异常的调用方。
- 将错误对象视为
instanceof Error——普通的判别对象并不是Error,因此instanceof检查会悄无声息地走错分支。
行为出乎意料:
- 丢弃 Result 也能顺利编译。类型系统不会标记你丢弃的失败。
map使用一个自身返回 Result 的函数时,会得到Result<Result<T, F>, E>——类型检查会通过,但调用方必须解包两次才能取出内容。flatMap正是为避免这种双重嵌套而存在。flatMap会合并错误类型,因此较长的链式调用最终会得到一个没人想处理的错误联合类型——当额外的变体不再重要时,用mapError将其收窄。- 对于没有成功值的操作,应使用
Result<void, E>,而不是Result<undefined, E>;后者会迫使调用方为一个不存在的值命名。 Promise<Result<T, E>>在等待期间也是真值,因此未等待的 Promise 会通过一个毫无意义的ok检查。- 在边界处重新抛出会丢弃类型化错误——调用方原本可以据此分支处理的原因,最终只剩下一个字符串。
combineWithAllErrors返回一个数组,而显示时只用到第一个元素,会浪费已有的工作;要么显示全部错误,要么快速失败。- 预先创建错误常量虽然省去了一次分配,却丢失了本可写入其中的上下文。