第22章:エラー分類(ドメイン vs インフラ)🚧🌐
この章のゴール🎯✨
- 失敗を「種類」で分けて整理できるようになる🧠🗂️
- どこで投げて、どこで受け止めて、どこで“言い換える”かが分かる🔁💬
- ドメインイベント設計で「壊れにくい失敗の扱い」を作れるようになる🛡️📣
1. まず結論:失敗は“2種類+α”に分けるのがラク😺🍀

✅ ドメインエラー(Domain Error)🧱🔒
業務ルール違反・状態が許されないが原因。 例:
- すでに支払い済みなのに、もう一回支払おうとした💳❌
- 在庫が0なのに注文しようとした📦❌
- クーポン期限切れなのに適用しようとした🎫⏰❌
👉 ユーザーに説明できる(そしてユーザーの行動で回避できる)ことが多いよ✨
✅ インフラエラー(Infrastructure Error)🌐🧯
通信・DB・外部サービスなど “環境” が原因。 例:
- DBに接続できない🗄️🔌❌
- メール送信APIがタイムアウトした📩⏳❌
- 決済サービスが一時的に落ちてる💳⚡❌
👉 ユーザーが頑張っても直らないことが多いよ😵💫
✅ そして“α”:未知のエラー(Bug/Unknown)🐛❓
- ぬるぽ、型ミス、想定外のデータ形式…など 👉 これは「分類できない」じゃなくて、まずバグとして扱うのが安全🙆♀️
2. なんで分類が必要なの?(超だいじ)💡💖
分類できると、判断が一気にラクになるよ✨
-
ドメインエラー:
- 画面に出す文言を用意しやすい🪧
- リトライしても意味がない(ルール違反は直らない)🔁❌
-
インフラエラー:
- リトライ対象になりやすい(次章で深掘り🔁🧯)
- ログ・監視・運用の対象になる👀📈
-
未知のエラー:
- まずアラート&調査🔔🕵️♀️
3. 3秒で分類するコツ⏱️🧠✨
迷ったらこの質問👇
Q1:これは「業務ルールの話」?🧾
- YES 👉 ドメインエラー🧱
- NO 👉 次へ
Q2:これは「外部・環境の話」?(DB/通信/依存サービス)🌐
- YES 👉 インフラエラー🌐
- NO 👉 次へ
Q3:それ以外(想定外)?🐛
- YES 👉 未知のエラー(バグ扱い)🐛
4. “境界で変換する”ってどういうこと?🔁🚪
ざっくり図にするとこう👇
- ドメイン層:ルール違反ならドメインエラーを出す🧱
- インフラ層:通信やDB失敗ならインフラエラーとして包む🌐
- アプリ/UI層(境界):受け取ったエラーを「画面・HTTP・ログ向け」に言い換える💬🗂️
5. TypeScriptの“今どき”ポイント(最新寄り)🧷✨
5.1 catch (err) の err は unknown が基本ムード🙋♀️
TypeScriptには useUnknownInCatchVariables というフラグがあり、catch 変数を unknown として扱う方針が用意されてるよ(狙いは「安全に絞り込め!」ってこと)🛡️✨ (TypeScript)
だから、catchした瞬間に err.message を触るのはNGになりがち😿
→ ちゃんと絞り込み(narrowing)しようね!
5.2 “原因を持たせる” Error.cause が超便利🧠🔗
エラーを「包み直す」ときに、元エラーを cause に残せるよ✨
- ブラウザでも広く使える仕様になってる🌍 (MDN Web Docs)
- Node.jsでも
error.causeがあり、new Error(message, { cause })で扱える(Nodeでは v16.9.0 追加)📌 (Node.js)
これがあると「表のメッセージは分かりやすく、でも原因は失わない」ができる😻
6. 実装してみよう:エラー型を2系統に分ける🧩🧱🌐
ここでは “例外(throw)で表現する版” を作るよ(まずはシンプル優先)✨
6.1 ドメインエラー基底クラス🧱
// domain/errors/DomainError.ts
export abstract class DomainError extends Error {
readonly category = "domain" as const;
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
this.name = new.target.name;
}
}
例:支払い済みなのに支払い、を禁止💳❌
// domain/errors/OrderAlreadyPaidError.ts
import { DomainError } from "./DomainError";
export class OrderAlreadyPaidError extends DomainError {
constructor(orderId: string) {
super(`注文(${orderId})はすでに支払い済みだよ💳✅(二重払い防止)`);
}
}
6.2 インフラエラー基底クラス🌐🧯(cause で原因を保持)
// infrastructure/errors/InfrastructureError.ts
export abstract class InfrastructureError extends Error {
readonly category = "infrastructure" as const;
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
this.name = new.target.name;
}
}
例:決済API呼び出し失敗を “文脈つき” で包む💳🌩️
// infrastructure/errors/PaymentGatewayError.ts
import { InfrastructureError } from "./InfrastructureError";
export class PaymentGatewayError extends InfrastructureError {
constructor(orderId: string, options?: { cause?: unknown }) {
super(`決済処理に失敗したよ💳⚠️ orderId=${orderId}`, options);
}
}
causeを使うと、表のメッセージを変えても「元の失敗(タイムアウト等)」を失わないよ🔗✨ (MDN Web Docs)
7. どこで投げる?どこで変換する?(ミニECで流れを作る)🛒🧩
7.1 ドメイン(集約)では「ルール違反だけ」を投げる🧱
// domain/order/Order.ts
import { OrderAlreadyPaidError } from "../errors/OrderAlreadyPaidError";
type OrderStatus = "pending" | "paid";
export class Order {
private status: OrderStatus = "pending";
private domainEvents: Array<{ type: string; payload: unknown }> = [];
constructor(private readonly id: string) {}
pay(paymentId: string) {
if (this.status === "paid") {
throw new OrderAlreadyPaidError(this.id);
}
this.status = "paid";
// ドメインイベントは「起きた事実」📣
this.domainEvents.push({
type: "OrderPaid",
payload: { orderId: this.id, paymentId },
});
}
pullDomainEvents() {
const events = this.domainEvents;
this.domainEvents = [];
return events;
}
}
ポイント💡
- ドメインは 通信/DBを知らない 🙅♀️
- だから、ここで出るのは ドメインエラー中心 🧱✨
7.2 インフラ(外部I/O)では「失敗を包んで投げる」🌐
// infrastructure/payment/PaymentGateway.ts
import { PaymentGatewayError } from "../errors/PaymentGatewayError";
export class PaymentGateway {
async charge(orderId: string, amount: number) {
try {
// ここで外部API呼び出しをする想定
// await fetch(...)
// わざと失敗例:
throw new Error("timeout");
} catch (err) {
// err は unknown になりがちなので絞り込みが大事🛡️ :contentReference[oaicite:4]{index=4}
throw new PaymentGatewayError(orderId, { cause: err });
}
}
}
7.3 境界(アプリ層)で “言い換え” する🔁💬
ここが第22章の本体✨ 「受け取ったエラー」を、呼び出し元が扱いやすい形に変換するよ!
まず “アプリ向けエラー” を定義🧾
// application/errors/AppError.ts
export type AppError =
| { kind: "validation"; message: string } // ユーザーに説明しやすい
| { kind: "temporary"; message: string } // 一時的(リトライ候補)
| { kind: "unexpected"; message: string }; // バグ/想定外
変換関数(ここが境界での翻訳機)🗣️🔁
// application/errors/toAppError.ts
import { DomainError } from "../../domain/errors/DomainError";
import { InfrastructureError } from "../../infrastructure/errors/InfrastructureError";
import { AppError } from "./AppError";
export function toAppError(err: unknown): AppError {
if (err instanceof DomainError) {
return { kind: "validation", message: err.message };
}
if (err instanceof InfrastructureError) {
// ユーザーにそのまま出すのは危険なことが多いので、文言は控えめに✨
return { kind: "temporary", message: "ただいま混み合ってるみたい…💦 もう一度ためしてね🙏" };
}
if (err instanceof Error) {
return { kind: "unexpected", message: "想定外のエラーが起きたよ😵💫(運営に連絡してね)" };
}
return { kind: "unexpected", message: "想定外のエラーが起きたよ😵💫" };
}
8. ドメインイベント的に、分類が効く場面📣🧩
✅ ドメインエラーが起きたら
- 状態変更できない
- 当然イベントも出ない(“起きてない”から)📣❌
✅ インフラエラーが起きたら(保存・送信・外部連携)
- “起きた事実” はあるのに、外へ届けられないことがある😿
- だから Outbox(第20〜21章)や、失敗時方針(第23章)に繋がる🔗✨
この章ではまず 「それはどっちの失敗?」 をブレずに言えるようにしようね🎯
9. やってみよう演習📝💖
演習1:分類ゲーム(5問)🧠🎮
次を domain / infrastructure / unknown に分けてみてね👇
注文合計が0円の注文を作ろうとした🧾DB接続が切れて保存できない🗄️メール送信APIが 503 を返した📩すでに発送済みなのにキャンセルしようとした📦JSON.parseが落ちた(想定外形式が来た)🧩💥
演習2:境界で変換して返す🔁💬
toAppError() を使って、ユースケースから戻す形を作ってみよう✨
// application/usecases/payOrder.ts
import { toAppError } from "../errors/toAppError";
import { AppError } from "../errors/AppError";
export async function payOrder(): Promise<{ ok: true } | { ok: false; error: AppError }> {
try {
// ここで、ドメイン更新 → 保存 → イベント配送…という流れを呼ぶ想定
// 例:order.pay(); repo.save(order); dispatcher.dispatch(order.pullDomainEvents());
return { ok: true };
} catch (err) {
return { ok: false, error: toAppError(err) };
}
}
10. AI活用(コピペで使える)🤖✨💬
- 「ミニECで起こりうるドメインエラーを10個、イベント名も添えて出して」🧾📣
- 「この失敗ケース一覧を domain / infrastructure / unknown に分類して、理由も一言で」🧠🗂️
- 「InfrastructureError に包むときの message のテンプレを3案(ログ向け/運用向け/画面向け)で」🌐🧯
- 「toAppError の分岐、漏れてるケースある?追加するなら何?」🔍✅
11. まとめチェックリスト✅🧡
- ドメイン層で投げるのは 業務ルール違反 だけ🧱
- 通信/DB/外部APIの失敗は インフラエラー として包む🌐
catchはunknown前提で 絞り込みする🛡️ (TypeScript)Error.causeで 原因を保持して、調査力を上げる🔗✨ (MDN Web Docs)- 境界(アプリ層)で “言い換え” して、呼び出し元に返す🔁💬