第16章:エラーも契約①:エラーの分類と境界😵💫➡️🙂

この章でできるようになること🎯✨
- 「この失敗、どの種類のエラー?」を3分類で言えるようになる🗂️
- “境界(boundary)”でエラーを整えて、利用者に優しい形に変換できるようになる🎁
catchの中で安全に扱えるように、型の守り方がわかる🛡️
1) まず大事な考え方:エラーも“契約”の一部だよ📜⚠️
関数やAPIって「入力したら、こう返すよ」って約束(契約)をしてるよね🤝 で、成功だけじゃなくて失敗も約束の一部なんだよ〜!
たとえば👇
- ✅ 成功:
Userが返る - ❌ 失敗:
ユーザーが存在しない/入力が変/DBに繋がらない…など
ここがふわっとしてると、使う側は毎回こうなる😇
- 「どんな失敗があり得るの?」
- 「どれは直せる?どれは待つしかない?」
- 「ログはどう出すの?」
だからこの章は、**“失敗の種類を整理して、境界で整える”**の練習をするよ💪✨
2) エラー3分類🧁🧠(この教材ではこの3つに固定!)
この章ではエラーを次の3つに分けるよ📛
A. ドメインエラー(Domain Error)🏷️💡
意味:ビジネス的に「それはダメだよ」っていう失敗 例:
- すでに退会済みのユーザーが「退会する」ボタン押した
- 残高が足りないのに購入しようとした
- 同じメールアドレスで新規登録しようとした
✅ 特徴:入力が正しくても起きる(ルール違反だから) 👉 利用者に“直し方”を案内できることが多いよ🫶
B. 入力エラー(Validation / Input Error)📝🚫
意味:入力の形・型・必須項目などがダメ 例:
emailが空ageが文字列で来たpageが-1- JSONの必須フィールドが欠けてる
✅ 特徴:ほぼ利用者が直せる 👉 だから エラー文は“直しやすさ”が超大事(第17章で本格的にやるよ🍰)
C. インフラエラー(Infrastructure Error)🌩️🔌
意味:ネットワーク・DB・外部API・ファイルなど、外部要因の失敗 例:
- DB接続失敗
- タイムアウト
- 外部APIが落ちてる
- 権限不足でファイル読めない
✅ 特徴:利用者が直せないことも多い 👉 だから “再試行してね” とか “後でもう一回” が現実的な案内になりがち🙂
3) 境界(boundary)ってなに?🧱🚪
境界はカンタンに言うと、こういう“つなぎ目”のこと!
- モジュールとモジュールの境目
- ドメイン層 ↔ アプリ層の境目
- サーバ内部 ↔ HTTPレスポンスの境目
- 自分のコード ↔ 外部ライブラリの境目
ここでやることは1つだけ👇 ✅ 内部で起きたごちゃっとした失敗を、外に出す前に“整形(翻訳)”する🎀
4) なんで境界で整えるの?(やらないと起きる悲劇😱)
もし境界で整えないと…
DB_CONNECTION_FAILEDがそのまま画面に出てユーザー困惑😵- どの失敗も
Error: something went wrongで、呼び出し側が詰む🧊 - ログに必要情報が残らず、デバッグ地獄🌀
なのでこの章の合言葉は👇 **「内部は自由、外向きは整える」**🪄✨
5) TypeScriptで“分類”をコードに落とす🟦🧩
5-1) catch の中は“unknown”が基本だよ🕵️♀️
catch (e) の e を unknownとして扱う設定があるよ。これを使うと、雑に e.message とか触れなくなるので安全になる🛡️
(useUnknownInCatchVariables)(typescriptlang.org)
狙い:
- 「throwされるものはErrorとは限らない」現実にちゃんと対応する
- 例外処理が“ちゃんと確認してから扱う”スタイルになる✨
5-2) 3分類を“型”で表現する(おすすめは判別しやすい形)🧷✨
「種類」が一瞬でわかるように、**識別子(kind)**を入れておくのが超おすすめ🎀
type DomainError = {
kind: "domain";
code: "USER_ALREADY_DELETED" | "INSUFFICIENT_BALANCE";
message: string; // 利用者向けの短い説明(第17章で磨く✨)
};
type InputError = {
kind: "input";
field?: string; // どこがダメ?が分かると親切🧡
message: string;
};
type InfraError = {
kind: "infra";
service: "db" | "externalApi" | "file";
message: string; // 外向けは控えめに(詳細はログへ)
};
type AppError = DomainError | InputError | InfraError;
ここまで作ると、境界で👇みたいに分岐できるようになるよ🥳
6) 境界でやる“翻訳”の基本パターン🧠➡️🧾
パターン①:外部ライブラリの例外をキャッチして、インフラエラーにする🌩️
内部ではライブラリがいろんな形でthrowしてくるから、境界で統一する!
そして大事なのが 原因(cause)をつなげること💡
new Error(message, { cause }) で「元の失敗」を保持できるよ。(MDN Web Docs)
function toInfraError(e: unknown): InfraError {
// まずは「何が投げられたか分からない」前提で安全に扱う
if (e instanceof Error) {
return {
kind: "infra",
service: "db",
message: "データ取得に失敗しました",
};
}
// Errorですらないものが投げられることもある😇
return {
kind: "infra",
service: "db",
message: "予期しない失敗が発生しました",
};
}
async function loadUser(userId: string) {
try {
// ここは例:DBアクセス
// await db.user.find(...)
} catch (e) {
// 内部の原因は cause としてつなぐ(ログや調査が楽になる✨)
throw new Error("loadUser failed", { cause: e });
}
}
ポイント🎀
- 利用者向けメッセージと、内部原因(cause)を分ける
- causeで“根っこ”を残すと、調査がめちゃくちゃ楽になる🕵️♀️✨ (MDN Web Docs)
パターン②:アプリの外(HTTPなど)へ出す直前で、3分類→“外向けの形”に整える🌐🎁
HTTP APIの場合、エラー応答の形式を標準化する方法として Problem Details(RFC 9457)があるよ。(datatracker.ietf.org) (本格的なAPI契約は後の章でやるけど、境界の考え方としてチラ見せ👀✨)
例イメージ👇
input→ 400domain→ 409(競合)や 422 など(方針次第)infra→ 503(サービス不可)や 500
7) “どこまで外に見せる?”の線引きルール📏✨
ここ、初心者が一番迷うところ!なのでシンプルに覚えよう🧠💕
- 入力エラー:なるべく詳しく(どこがダメか)📝
- ドメインエラー:ルール違反が分かるように(でも内部事情は出しすぎない)🏷️
- インフラエラー:外には控えめ(詳細はログ)🌩️
つまり👇 外向け:短く・直し方が分かる 内向け:原因が追える(ログ・cause・ID) この二刀流が強いよ⚔️✨
8) ミニ演習:エラー一覧を3分類してみよう📛🗂️(15〜25分)
あなたのアプリ(または架空のアプリ)で、次を作ってみてね🫶
ステップ1:エラー候補を10個書く📝
例:ログイン/商品購入/ファイルアップロード など、好きな題材でOK🎀
ステップ2:3分類に仕分ける🗂️
- ドメイン:___
- 入力:___
- インフラ:___
ステップ3:境界を1個決める🚪
例:
controller(HTTPの入口)service(アプリ層の入口)repository(DBの入口)
そこで「外に出す形」をどう整えるか、軽くメモする✍️✨ (第17章で“優しい返し方”をガッツリ磨くよ🍰)
9) AI活用プロンプト例🤖💬(コピペOK✨)
- 「この機能で起こり得るエラーを ドメイン/入力/インフラ に分類して、表にして」🗂️🤖
- 「この例外処理、境界で翻訳できてる?改善案を3つ出して」🚪🤖
- 「
catchで unknown を安全に扱うガード関数を提案して」🛡️🤖 - 「このエラー一覧、利用者に見せていい情報/ダメな情報を分けて」🔒🤖
10) まとめ✅✨(この章の持ち帰り)
- エラーは“失敗時の契約”だから、ふわっとさせない📜⚠️
- まずは ドメイン/入力/インフラ の3分類で整理する🗂️
- 境界で翻訳して「外向けは分かりやすく、内側は調査しやすく」🎁🔍
catchは unknown前提で安全に扱うのが強い🛡️(typescriptlang.org)- 原因を残すなら
Errorのcauseが便利💡(MDN Web Docs)