第19章:“境界で変換する”:内部エラーを外に漏らさない🧱🔁
この章でできるようになること🎯✨
- 「境界(Boundary)」で例外を受け止めて、外に出す形へ変換できるようになる🧤📦
- ユーザー向けメッセージとログ向け詳細を分けて、安全&デバッグしやすくする🧾🔐
unknownをちゃんと扱って、**“何が飛んできても落ち着いて処理する”**を書けるようになる🧘♀️🧠causeを使って「原因」を保持しつつ、外側はスッキリにできる🪢✨(Error.causeは広く利用可能💡) (MDN Web Docs)
今どきメモ(2026年1月)🗓️🧩
- TypeScript は 5.9.2(Stable) が公開されているよ📌 (GitHub)
- VS Code は v1.108(2025年12月アップデート) が 2026-01-08 にリリース📌 (Visual Studio Code)
- Node.js は v24.13.0(LTS) が 2026-01-13 に出ているよ🔒 (Node.js)
- そして TypeScript は「ネイティブ移植(プレビュー)」でビルド高速化の流れも進行中🏎️💨 (Microsoft Developer)
なぜ「境界で変換」しないと危ないの?😱🧨
境界で変換しないと、こんな事故が起きがち👇
- 内部情報ダダ漏れ(DB名、SQL、ファイルパス、スタックトレース…)🫣💥
- 画面やAPIで 意味不明なエラー が出て、ユーザーも開発者も困る🥲❓
- 例外の種類がバラバラで、呼び出し側が 毎回 try/catch 地獄 になる🌀🧟♀️
- 「直す場所」が増えて、設計がどんどん崩れる🏚️💦
だから基本はこれ👇 中の例外は中で使う。外には“外向けの形”だけを出す。 🚧➡️📦
「境界」ってどこ?🚧🧭
境界は、世界が変わるところだよ🌍✨ たとえば👇
- HTTP API の入口(ルーティング、コントローラ)🌐🚪
- CLI の入口(コマンド実行)⌨️🚪
- UI のイベントハンドラ(ボタン押下)🖱️🚪
- ジョブ実行の入口(スケジューラ、キュー)⏰📥
境界の仕事はざっくり2つだけ👇
- 入力を受ける(変換・検証)🧼
- 失敗を返す(外向けに変換)📤
この章は 2) を主役にするよ💃✨
変換の基本ルール3つ🏁✅

ルール①:外に出すエラーは「型(形)」を固定する📦🧱
- 例:
AppErrorみたいな 統一フォーマット にする - UI/HTTP/CLI など、外側の都合に合わせて 出力を作れるようになる✨
ルール②:メッセージは二重にする🧾🔐
- ユーザー向け:短く・安全・次にやることが分かる🙂🧭
- ログ向け:原因・スタック・文脈・再現に必要な情報🕵️♀️📌
ルール③:境界以外では「変換しない」🙅♀️
- 中で無理に握りつぶすと、原因が迷子になる🐶💨
- 変換は境界の仕事!中は “正確に投げる” に集中🎯
まずは最小の土台:AppError を作ろう🧩✨
「外に出す形」を決めるよ📦 (HTTPでもCLIでも同じ思想で使えるよ👍)
// AppError = 外向けの統一フォーマット📦
export type AppError =
| {
kind: "BadRequest";
code: "BAD_REQUEST";
message: string;
details?: unknown; // 入力エラーなど(外に出してOKな範囲だけ)
}
| {
kind: "Conflict";
code: "CONFLICT";
message: string;
}
| {
kind: "NotFound";
code: "NOT_FOUND";
message: string;
}
| {
kind: "Unauthorized";
code: "UNAUTHORIZED";
message: string;
}
| {
kind: "Internal";
code: "INTERNAL";
message: string; // ユーザー向けは固定文言が基本🧊
cause?: unknown; // ログ用に保持(外へは出さない前提)
};
ポイント💡
kindを 判別用(discriminated union) にしておくと分岐が超ラク🥳Internalは 固定メッセージ 推奨(漏洩防止)🔐causeはログ用に抱えとく(これ超大事)🪢✨
「unknown が来ても平気」な変換関数を作る🧯✨
catch で受けるのは、基本 unknown として扱うのが安全🙆♀️
(useUnknownInCatchVariables: true だとより安心🧼)
// 便利:安全に Error 判定する🧠
const isError = (e: unknown): e is Error => e instanceof Error;
// 例:ドメイン側で投げる「仕様上の失敗」用(第17〜18章の続き感)
export class DomainError extends Error {
constructor(
public readonly domainCode: "EMAIL_TAKEN" | "INSUFFICIENT_BALANCE",
message: string
) {
super(message);
this.name = "DomainError";
}
}
// 変換の本体:unknown → AppError📦
export const toAppError = (e: unknown): AppError => {
// 仕様として起きる失敗(ユーザー向けに出してOK)
if (e instanceof DomainError) {
if (e.domainCode === "EMAIL_TAKEN") {
return { kind: "Conflict", code: "CONFLICT", message: "そのメールは既に使われています📩💦" };
}
return { kind: "BadRequest", code: "BAD_REQUEST", message: e.message };
}
// よくある「見つからない」
if (isError(e) && e.name === "NotFoundError") {
return { kind: "NotFound", code: "NOT_FOUND", message: "見つかりませんでした🔍💦" };
}
// それ以外は Internal(詳細はログへ)
return {
kind: "Internal",
code: "INTERNAL",
message: "エラーが発生しました。時間をおいて再度お試しください🙏💦",
cause: e,
};
};
ここが気持ちいいところ🥰✨
- 外に返すメッセージは統一できる
- でも
causeに “本当の原因” を保持できる - だから、ユーザーには安全、開発者には優しい💖🛠️
cause で「原因」をつないでいく🪢✨
境界で変換するなら、原因チェーンが命!🔥
Error.cause は「捕まえたエラーを別エラーに包む」用途で使われるよ🧠 (MDN Web Docs)
export class InfraError extends Error {
constructor(message: string, options?: { cause?: unknown }) {
// ES2022+ なら cause を付けられる(環境設定はプロジェクト側で)
super(message, options);
this.name = "InfraError";
}
}
例:DB層で catch して “DBの文脈” を付けて投げ直す👇
async function fetchUserFromDb(userId: string) {
try {
// ...DBアクセス
} catch (e) {
throw new InfraError("DBアクセスに失敗しました", { cause: e });
}
}
これでログに出すとき👇
- 表面:
DBアクセスに失敗しました - 原因:接続失敗/タイムアウト/SQLエラー… が辿れるようになるよ🕵️♀️🔗
境界でのテンプレ:try/catch はここだけに寄せる🧱✨
境界(例:HTTPハンドラっぽい入口)ではこう👇
type HttpResponse =
| { status: 200 | 201; body: unknown }
| { status: 400 | 401 | 404 | 409 | 500; body: { message: string; code: string } };
const toHttpResponse = (err: AppError): HttpResponse => {
switch (err.kind) {
case "BadRequest":
return { status: 400, body: { message: err.message, code: err.code } };
case "Unauthorized":
return { status: 401, body: { message: err.message, code: err.code } };
case "NotFound":
return { status: 404, body: { message: err.message, code: err.code } };
case "Conflict":
return { status: 409, body: { message: err.message, code: err.code } };
case "Internal":
return { status: 500, body: { message: err.message, code: err.code } };
}
};
const logAppError = (err: AppError, context: { requestId: string }) => {
// 例:ログはここで集約🧾
// cause は外へ返さないけど、ログには残せる📌
if (err.kind === "Internal") {
console.error("🔥INTERNAL", context, { err, cause: err.cause });
} else {
console.warn("⚠️APP_ERROR", context, { err });
}
};
境界でのテンプレ🧱🔁

/**
* 境界(コントローラ/ハンドラ)での書き方
*/
async function handleRequest(input: unknown) {
try {
// 1. バリデーション & 中心ロジック呼び出し
const result = await someCoreLogic(input);
return { status: 200, body: result };
} catch (e) {
// 2. 境界で「外向けの言葉」に変換する!
const errorResponse = translateError(e);
return { status: errorResponse.status, body: errorResponse.body };
}
}
export async function handleCreateUser(requestId: string, input: unknown): Promise<HttpResponse> {
try {
// ここでは「成功時の流れ」に集中✨
const userId = await createUserUseCase(input);
return { status: 201, body: { userId } };
} catch (e) {
// 変換は境界の仕事🧱
const appErr = toAppError(e);
logAppError(appErr, { requestId });
return toHttpResponse(appErr);
}
}
// 例の中身(ここでは適当に)
async function createUserUseCase(_input: unknown): Promise<string> {
// ... 中では DomainError / InfraError を投げてもOK
return "user_123";
}
この形のメリット🥳🎉
- 入口はスッキリ(成功ルートが読みやすい)✨
- 失敗は 必ず
AppErrorに統一される📦 - レスポンスもログも “ルールで処理” できる🧠✅
やっちゃダメ集(事故りやすい)🚫💥
return { error: e }みたいに 例外をそのまま返す(情報漏洩)🫣console.log(e)だけして 握りつぶす(原因迷子)🐶💨- “中の層” で UI/HTTP形式に変換しちゃう(関心の混線)🧶🌀
- ユーザー向けメッセージに
e.messageをそのまま使う(危険)🔓💣
演習①:エラー変換関数を強化しよう🧪✨
次の仕様で toAppError を改造してね✍️
DomainError("INSUFFICIENT_BALANCE", "...")はBadRequestにして、メッセージは「残高が足りません💸😢」に固定するErrorでname === "AbortError"はInternalにして「時間がかかりすぎたので中断しました⌛💦」にする- それ以外は今のままでOK🙆♀️
演習②:境界で「返す」「ログる」を完全に分離しよう🧩🧾
handleCreateUser の catch をこう分けてみてね👇
handleError(e, ctx)はAppErrorを返す(変換担当)📦respondError(appErr)はHttpResponseを返す(出力担当)📤logError(appErr, ctx)はログだけ(ログ担当)🧾
「役割が分かれると、設計が急にキレイになる」感覚が掴めるよ🥰✨
AI活用メモ(雛形づくりに便利)🤖⚡
- 「
AppErrorの union とtoHttpResponseの switch を作って」 - 「
toAppErrorのテストケース(DomainError/unknown/AbortError)を列挙して」 - 「
logAppErrorに requestId と cause を入れて、漏洩しない形にして」
雛形はAIが速い🏎️💨 でも “何を外に出してOKか” の判断は人間が責任持つのが大事だよ⚖️🧠
章末チェックリスト✅📌
- 境界でだけ
try/catchしている🚧 - 外向けの失敗は
AppErrorに統一した📦 - ユーザー向けとログ向けを分けた🔐🧾
-
unknownを安全に扱えている🧼 -
causeで原因をつないで追跡できる🪢🔍 (MDN Web Docs)