mcpField
W
web-error-handling-result-types

web-error-handling-result-types

github.com/agents-inc开发2668 次安装2 天前更新
代码仓库

关于 web-error-handling-result-types

面向 TypeScript 的技能,使用带判别字段的 Result 类型表示预期失败,避免错误仅隐藏在异常流程中。提供类型化错误、同步与异步组合及合并结果的模式和参考资料。

主要功能

  1. 01定义 Result 类型与构造函数
  2. 02用 map 和 flatMap 组合操作
  3. 03用 tryCatch 转换可能抛错的调用
  4. 04通过判别联合建模错误
  5. 05合并结果,支持快速失败或收集全部错误

适用场景

  1. 01表示验证与解析失败
  2. 02串联请求或其他可能失败的步骤
  3. 03收集表单中的多项验证错误
  4. 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.yaml
reference.md
examples/
async.md
combining.md
core.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);
};

完整代码:examples/combining.md


<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 返回一个数组,而显示时只用到第一个元素,会浪费已有的工作;要么显示全部错误,要么快速失败。
  • 预先创建错误常量虽然省去了一次分配,却丢失了本可写入其中的上下文。

评论

支持 Markdown