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. 01Result 型とコンストラクターの定義
  2. 02map と flatMap による処理の合成
  3. 03tryCatch による例外発生処理の変換
  4. 04判別可能なユニオンによるエラー表現
  5. 05早期失敗または全件収集での結果結合

活用シーン

  1. 01検証や解析の失敗の表現
  2. 02リクエストなど失敗し得る処理の連鎖
  3. 03フォームの複数の検証エラーの収集
  4. 04API 境界での型付き失敗の処理

インストール

コマンドをコピーしてターミナルで実行します。

$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 を読み取ることを許しません。これにより、例外では隠れてしまう関数の失敗モードがシグネチャに表れます。検証、解析、リクエストなど、想定される失敗に使用し、バグや後続の処理では対処できない条件には例外を使います。カスタム実装は約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 などは戻り値の型をすり抜けて例外をスローするため、ラップされていない呼び出しが1つでもあると、シグネチャが実態と異なり、呼び出し側の網羅的なエラー処理も不完全になります。

各エラーには 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、例外、または nullable

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

エラーが code: "NOT_FOUND" だけの Result<User, NotFoundError> は、仮装した nullable です。呼び出し元が理由によって次のアクションを変える場合に、Result を選びます。

早期失敗か、すべて収集するか: フォームのフィールドが1つ無効だからといって、ほかの4つを隠す理由にはならないため、フォームのバリデーションでは収集します。一方、ステップ2がステップ1の出力を使うチェーンには、収集するものがないため、途中で処理を打ち切ります。

また、値を返すほうが例外を投げるよりはるかに低コストです。例外を投げるとスタックトレースを取得して巻き戻しが発生するためです。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

1つの式の中で両方の側に答えるため、外向きの境界で自然に変換できます。

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 になります。コンパイラーを通すために非 null アサーションや型キャストが使われています。
  • Result を返す関数内で、例外を投げる呼び出しをラップせずに残すと、その例外は「捕捉するものはない」と伝えられていた呼び出し元に伝播します。
  • エラーオブジェクトを instanceof Error で判定することです。単純な判別用オブジェクトは Error ではないため、instanceof チェックによって誤った分岐がひそかに選ばれます。

予想外の動作:

  • Result を破棄しても、コンパイルエラーにはなりません。型システム上、破棄した失敗を示すものはありません。
  • Result を返す関数を map に渡すと、Result<Result<T, F>, E> になります。型チェックは通りますが、呼び出し元は値にたどり着くまでに二度アンラップする必要があります。この二重化を防ぐために flatMap があります。
  • flatMap はエラー型を結合するため、長いチェーンの末尾では、扱いたくないエラー型のユニオンが生じます。追加のバリアントを考慮する必要がなくなった時点で、mapError を使って型を絞り込んでください。
  • 成功値のない操作では、Result<undefined, E> ではなく Result<void, E> を使います。後者では、存在しない値を呼び出し元が指定しなければなりません。
  • Promise<Result<T, E>> は処理中でも truthy であるため、await していない Promise は意味のない ok チェックを通過します。
  • 境界で再スローすると、型付きエラーが失われます。呼び出し元が分岐に使えた理由が、単なる文字列になってしまいます。
  • combineWithAllErrors が返す配列のうち、表示するのが先頭の要素だけなら、残りを処理する手間が無駄になります。すべて表示するか、最初のエラーで失敗させてください。
  • あらかじめ作成したエラー定数は、生成コストを節約できる一方で、そこに含められたはずのコンテキストを失います。

コメント