メインコンテンツまでスキップ

第25章:エラー設計(ドメイン vs インフラ)😇🚨

testable_ts_study_025_error_sorting.png

この章は、「失敗」をちゃんと“設計の一部”として扱えるようになる回だよ〜!💪✨ (※ 2026/01/16 時点の情報として、TypeScript は npm の latest が 5.9.3 だよ〜。(npm))


25.0 ゴール🎯✨* 「この失敗は 仕様として起きる失敗?それとも 外部都合の事故?」を瞬時に分けられる🧠⚡

  • 失敗を “型” として表現して、テストが安定する形にできる🧪💎
  • 次章(第26章)でやる「境界で変換」がスムーズにできるようになる🔁🧯

25.1 まず結論:失敗は2種類に分けるとラク😌✨### A) ドメインエラー(仕様の失敗)

😇* 例:在庫が足りない、クーポンが無効、年齢制限に引っかかった、など

  • 業務ルールにより“起きて当然” な失敗
  • だいたい「ユーザーにそのまま伝えてOK」寄り📣

B) インフラエラー(外部都合の失敗)

🚨* 例:DB落ちた、ネットワークタイムアウト、外部APIが500返した、など

  • 外部システムの都合で起きる事故
  • ユーザーには「ごめん、今ムリ🙏」系の表現になりがち(詳細はログへ)🪵

この分け方自体が、いろんな設計で使われる超基本の考え方だよ〜。(fsharpforfunandprofit.com)


25.2 どっち?判定のコツ✂️

🧠迷ったら、この質問👇

✅「仕様として想定してる失敗?」* YES → ドメインエラー😇

  • NO → 次へ

✅「外部が落ちた/遅い/壊れた系?」* YES → インフラエラー🚨

✅「同じ入力で毎回同じ失敗になる?」* なりやすい → ドメイン寄り

  • ならない(気分で落ちる😇)→ インフラ寄り

25.3 設計ルール:エラーは“データ”として持つのが最強🧊✨

TypeScript だと特に、

  • throw を多用するより
  • 判別可能な union(discriminated union) で「エラーの種類」を型にする

これがめちゃ強い💪 switch で分岐すると、型がスッと絞れて安全になるよ〜。(TypeScript)


25.4 ハンズオン題材:注文(Place Order)

🛒🍕「注文する」って処理で起きそうな失敗を分けてみよ〜!✍️✨

ドメインエラー候補😇* カートが空

  • 在庫が足りない
  • クーポン無効
  • 支払いが拒否された(※“決済会社が正常に答えた結果として拒否”ならドメイン寄り)

インフラエラー候補🚨* 決済APIがタイムアウト

  • DB接続できない
  • 外部サービスが落ちた(5xx)

25.5 型を作る(ドメイン vs インフラ)

🧩✨ここからコードだよ〜!🧸 (※ 例は最小構成。ファイル分割してもOK!)

// 1) ドメインエラー(仕様の失敗)😇
export type DomainError =
| { type: "CartEmpty" }
| { type: "OutOfStock"; sku: string; requested: number; available: number }
| { type: "InvalidCoupon"; code: string }
| { type: "PaymentDeclined"; reason: "INSUFFICIENT_FUNDS" | "FRAUD" | "UNKNOWN" };

// 2) インフラエラー(外部都合)🚨
// ★ポイント:ユーザーに見せる文言じゃなく「状況」を持つ!
export type InfraError =
| { type: "NetworkTimeout"; service: "payment" | "inventory"; timeoutMs: number; retryable: true }
| { type: "DbUnavailable"; operation: "loadCart" | "saveOrder"; retryable: true }
| { type: "Unexpected"; message: string; retryable: false; cause?: unknown };

いい感じポイント🌟* type: "..." があるから switch で安全に分岐できる(型が絞れる)

🛡️(TypeScript)

  • インフラエラーに retryable を入れると、リトライ設計がラクになる🔁✨

25.6 Result型で「失敗も戻り値」にする📦🧪「throw じゃなくて戻り値で返す」やり方ね!

testable_ts_study_025_result_vs_throw.png TypeScript だと Result<T, E> を使うとテストが超安定するよ〜🧊

export type Ok<T> = { ok: true; value: T };
export type Err<E> = { ok: false; error: E };
export type Result<T, E> = Ok<T> | Err<E>;

export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
export const err = <E>(error: E): Err<E> => ({ ok: false, error });

ちなみに「Result型を提供するライブラリ」として neverthrow が有名だよ〜(ResultAsync もある)📦✨(GitHub) (この教材では自作でも全然OK!)


25.7 中心ロジックは「ドメインエラー」を返すのが基本🍰✨

中心(ロジック)は、できるだけ 外部の事故を知らない ほうがキレイ!🙈 なので、中心ロジックは「仕様としての失敗」を丁寧に返すのが王道だよ〜😇

import { Result, ok, err } from "./result";
import { DomainError } from "./errors";

type CartItem = { sku: string; qty: number; price: number };
type Cart = { items: CartItem[]; coupon?: string };

export function validateCart(cart: Cart): Result<Cart, DomainError> {
if (cart.items.length === 0) return err({ type: "CartEmpty" });

// 例:クーポンの形だけチェック(詳細は別関数でもOK)
if (cart.coupon && !/^[A-Z0-9]{6,10}$/.test(cart.coupon)) {
return err({ type: "InvalidCoupon", code: cart.coupon });
}

return ok(cart);
}

25.8 テストが「気分で落ちない」世界へ🧪🌈

ドメインエラーが Result で返ると、テストはこうなる👇

import { describe, it, expect } from "vitest";
import { validateCart } from "./validateCart";

describe("validateCart", () => {
it("カートが空なら CartEmpty 😇", () => {
const r = validateCart({ items: [] });
expect(r).toEqual({ ok: false, error: { type: "CartEmpty" } });
});

it("クーポン形式が変なら InvalidCoupon 😇", () => {
const r = validateCart({ items: [{ sku: "A", qty: 1, price: 1000 }], coupon: "!?!" });
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.error.type).toBe("InvalidCoupon");
}
});
});

テスト基盤としては、最近だと Vitest 4 系が大きめの更新が入ってるよ(4.0 のアナウンスや、GitHub のリリースで 4.0.17 などが確認できる)🧪⚡(Vitest)


25.9 よくある事故パターン(ここ注意!

)⚠️😵‍💫### ❌ 1) ドメインエラーを全部 throw にする* 使う側が毎回 try/catch 地獄になりがち😇🌀

  • “仕様の失敗” は戻り値で返すほうが読みやすいことが多い📦

❌ 2) インフラの例外メッセージを中心に持ち込む* DBのエラー文言とかが中心を汚す😵

  • 次章(第26章)で「境界で変換」するのがキレイ✨

❌ 3) エラーが「文字列」だけ* "error" とかにすると、後で分類不能で泣く😭

  • type + 必要データ、が正義🛡️(TypeScript)

25.10 章末ミニ課題📝

✨### 課題A:分類ゲーム🎮次を「ドメイン/インフラ」に分けてね👇

  1. 在庫が0だった
  2. 決済APIが502返した
  3. クーポン期限切れ
  4. DB接続タイムアウト
  5. 年齢制限NG

課題B:型を増やす✍️* DomainErrorTooManyItems を追加(例:上限 99 個)

  • InfraErrorRateLimited(429)を追加(retryAfterSec を持たせる)

25.11 AIに手伝ってもらう小ワザ🤖🎀AIには「列挙」と「抜け漏れチェック」が超得意だよ〜✨

プロンプト例👇

  • 「注文処理で起きうるドメインエラーを10個列挙して、ユーザー向け文言は書かず“状況データ”だけ提案して」
  • 「インフラエラーに入れるべきフィールド候補(retryable, service, operation…)を提案して」

(ただし “境界をどこに引くか” は自分が決めるのが大事!✂️🧠)


25.12 まとめ🎁

✨* 失敗は ドメイン(仕様)インフラ(外部事故) に分けると整理が爆速😇🚨(fsharpforfunandprofit.com)

  • TypeScript では 判別可能 union がエラー設計に相性バツグン🛡️(TypeScript)
  • Result<T, E> で返すとテストが安定して幸福度UP🧪🌈(neverthrow みたいな選択肢もあるよ)(GitHub)

次章予告👀✨

(第26章)次はついに! 境界でエラー変換(例外→結果、結果→表示) 🔁🧯 「インフラ例外を中心が扱える形に変換する」一本槍で、めちゃキレイに繋げるよ〜!🎀