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

第12章:バグ(不変条件違反)を分離する⚡🧱

〜「ここに来たらおかしい」を“設計として”扱えるようになるよ😊✨〜


0. この章でできるようになること🎯✨

この章のゴールはこれ👇💡

  • 「ドメインエラー」「インフラエラー」「バグ(不変条件違反)」を混ぜないで扱える🙅‍♀️🧺
  • 「ここに来たら絶対おかしい!」をコードで明文化できる✍️🧱
  • バグが起きたとき ユーザーに見せる情報開発者が欲しい情報 を分離できる👩‍💻🧑‍💻🔍
  • TSで「到達しないはず」の場所を never で守れる🛡️✨(網羅性チェック)

ちなみに、今どきのTSは TypeScript 5.9 が安定版として扱われています(GitHub Releases基準)📌 (GitHub)


1. まず結論:バグは「エラー」だけど、ユーザーに返す“失敗”じゃない🙅‍♀️⚡

いままでの章で出てきた分類を思い出してね🗺️✨

  • ドメインエラー💗:ユーザーの入力ミス / 業務ルール違反(想定内)
  • インフラエラー🔌:通信・DB・外部APIなど現実的に落ちる(想定内)
  • **バグ(不変条件違反)**🧱⚡:想定してない状態 / ここに来たらおかしい

ここ超大事👇😳 バグは「ユーザーが直すべき失敗」じゃないの。 だからユーザーに「あなたが悪いです」みたいに出したらダメ🙈💦


2. 「不変条件(Invariant)」ってなに?🧱🙂

不変条件は、ひとことで言うと👇

“この世界(このドメイン)では常に成り立っていてほしい条件” 🧱✨

例を出すね🧸🛒

  • 注文の合計金額は 0円以上 💴
  • カートの数量は 1以上 🧺
  • 「支払い済み」なら「支払いID」が 必ずある 💳
  • status"paid" なのに paidAtnullそれ、もう世界が壊れてる🧨😱

こういうのが崩れてたら、それは 入力ミス じゃなくて コードのどこかが壊してる可能性が高い → つまりバグ🧱⚡


3. Fail Fast(早く壊す)は“優しさ”だよ💥🫶

![バグは「隠すな、派手に壊せ」[(./picture/err_model_ts_study_012_fail_fast_siren.png)

不変条件違反を見つけたら、基本はこう👇

その場で止める(Fail Fast)原因が分かる情報を残すユーザーには安全な表示

Fail Fast は、ユーザーに冷たいんじゃなくて、むしろ逆😌💗 「壊れたまま動き続ける」方が、データ破壊・二次被害がヤバいの…😱🧨


4. ユーザー表示と開発者情報を分離しよう🎀🔍

バグが起きたときの基本方針はこれ👇✨

  • ユーザーに見せる:

    • 「問題が発生しました。時間をおいて再度お試しください」みたいな 安全メッセージ🙂🙏
  • 開発者が欲しい:

    • どの不変条件が破れた?
    • どの入力・状態だった?
    • どの経路でそこに来た?
    • 元の例外(cause)がある?

ここで便利なのが Error の cause だよ🎁🧵 いまは new Error("msg", { cause }) の形で「原因をぶら下げられる」✨ (MDN Web Docs)


5. TSで「不変条件」をコードにする3点セット🧰✨

5-1. ① invariant() を作る(Assertion Function)🛡️🙂

TSには「これが真じゃなかったら投げる」みたいな アサーション関数が作れるよ✨ asserts condition ってやつ(TS 3.7以降の仕組み)📌 (TypeScript)

export function invariant(
condition: unknown,
message: string,
options?: { cause?: unknown }
): asserts condition {
if (!condition) {
// cause があると、あとで原因追跡がめっちゃ楽🎁🧵
throw new Error(message, options);
}
}

使い方👇😊

invariant(cart.items.length > 0, "Invariant violated: cart.items must not be empty");

ポイント💡 invariant()“ユーザー入力チェック”に使わない のがコツ🙅‍♀️ ユーザー入力はドメインエラーとして返す(第10章の担当)💗


5-2. ② assertNever() で「到達しない」場所を守る🧱🛡️

TSの never を使うと、switchの網羅性チェックができるよ✨ 公式ハンドブックにも例があるやつ📌 (TypeScript)

export function assertNever(x: never): never {
throw new Error(`Invariant violated: unreachable case: ${String(x)}`);
}

例:注文ステータスの分岐👇🧾

type OrderStatus = "draft" | "paid" | "shipped";

function statusLabel(s: OrderStatus): string {
switch (s) {
case "draft": return "下書き";
case "paid": return "支払い済み";
case "shipped": return "発送済み";
default: return assertNever(s); // ここが“地雷センサー”💣✨
}
}

これ、未来に "canceled" を追加したのに分岐追加を忘れたら、 コンパイル時に気づける確率が爆上がりするよ😆💖


5-3. ③ Nodeの assert は「テストで使う」が相性良い🧪🧡

Nodeには node:assert っていう公式アサートもあるよ✨ (Node.js) ただ、アプリ本体で多用するより テストで使う のがやりやすいことが多いかな🙂


6. 具体例:同じ“失敗”でも分類が違うよ🧸🧠

ケース:在庫から引いて注文確定🛒📦

  • 在庫が0 → ドメインエラー(在庫なし)💗
  • DB接続が落ちた → インフラエラー🔌
  • 在庫が -3 になった → **バグ(不変条件違反)**🧱⚡(世界崩壊)

バグのときはこう考えるとラク👇😊 「それ、ユーザーが操作を変えたら直る?」

  • 直る → ドメイン寄り💗
  • 直らない / そもそも起きちゃダメ → バグ🧱⚡

7. ミニ演習📝✨(超大事!)

演習1:あなたのアプリの不変条件を3つ決めよう🧱🧠

例テンプレ👇(コピペOK)✨

  • 不変条件①:________(例:合計金額は0以上)
  • 不変条件②:________
  • 不変条件③:________

そして次も書く👇✍️ 「破ったらどうする?」

  • どこで検出する?(関数の入口?状態遷移?)🚪
  • どう止める?(throw?境界でキャッチ?)🧯
  • ユーザー表示は?🙂
  • 開発者ログは何が必要?🔍

演習2:invariant() を1箇所入れてみよう🛡️✨

おすすめはここ👇

  • 状態遷移の直前(例:pay() の中)💳
  • 集計結果の直後(例:合計金額)🧾

演習3:switchに assertNever() を入れてみよう💣✨

「ステータス」「種別」「モード」みたいな union があるところに入れると効果抜群😆


8. AI活用🤖💖(この章での“勝ち”プロンプト集)

コピペで使ってね😊✨

  • 「この処理にあり得ない状態(不変条件)を10個列挙して」🧱🔎
  • 「ドメインエラーとバグ(不変条件違反)の境界が曖昧な点を指摘して」⚖️👀
  • 「この union の switch を網羅的にするための assertNever パターンを書いて」🧩🛡️
  • 「ユーザー表示に出して良い情報 / ダメな情報を分けて、文言案も出して」🎀📝
  • 「この例外の cause を残す設計に変えて(Error cause を使って)」🎁🧵 (MDN Web Docs)

9. よくある事故💥🙅‍♀️(ここ超ひっかけ)

  • invariant()入力チェック に使ってしまう(=ユーザー操作で起きる失敗を“バグ扱い”にしちゃう)😵‍💫
  • any で型が崩れて、不変条件がすり抜ける🫥
  • 例外を握りつぶして「何も起きなかったこと」にする🙈(Fail Fastの逆😱)
  • バグ詳細をそのまま画面に表示(情報漏えい・UX最悪)🙅‍♀️💦

10. まとめ🎁✨(この章の合言葉)

  • バグ=不変条件違反=「ここに来たらおかしい」🧱⚡
  • 見つけたら Fail Fast 💥
  • ユーザー表示と開発者情報は 分離 🎀🔍
  • TSでは assertsnever が強い🛡️✨ (TypeScript)
  • cause で原因をつなぐと追跡がラク🎁🧵 (MDN Web Docs)

次章チラ見せ👀✨

次の第13章は「カスタムErrorの作り方(JS流)🧰✨」! ここで作る「エラーの型」が、分類(domain/infra/bug)を崩さない武器になるよ😆💖