TypeScript でエラーをドメイン概念として扱うのは「普通」か
TL;DR
- エラーをドメインとして扱うのは主流の考え方。関数型 DDD / Effect 系ではほぼ前提になっている。
- ただし対象はドメインエラー(業務上想定される失敗)のみ。インフラエラー・パニックまで型で表現しようとすると破綻する。
- 「Error クラスを継承するか」は直交する別の論点。継承しないタグ付きユニオンをドメインエラーとするのも十分「普通」。
- 判定基準は「ドメインエキスパートがこの失敗に名前を持っているか」「呼び出し側が種類ごとに違う対応をするか」の 2 つ。
このドキュメントの射程
「TypeScript で、よくある Error(クラスを継承していないものも含む)をドメインとして考えるのは普通か」という問いに対して、国内外の情報源を調べて整理したもの。
扱うのは以下 3 点。
- エラーをドメイン概念として扱う考え方の出典と普及度
- 「すべてのエラー」を対象にすると何が壊れるか
Error継承の有無がこの議論とどう関係するか
原因(なぜこの問いが生まれるか)
TypeScript の try…catch には以下の構造的な問題がある。
- 例外を投げうる関数かどうかが型シグネチャに現れない
catchの変数がunknown(設定次第でany)なので、種類に応じたハンドリングができないtry…catchブロックが散在して制御フローが不明瞭になる
この解決策として Result 型が注目され、その延長で「じゃあ Err 側に入れる型は何者なのか?」=「ドメインなのか」という問いが立つ。
解決(調査結果)
1. 出典は Wlaschin の 3 分類
『関数型ドメインモデリング』(Scott Wlaschin)がほぼ事実上の出典。エラーを次の 3 つに分類する。
| 分類 | 内容 | 扱い |
|---|---|---|
| ドメインエラー | 業務プロセスの一部として想定される失敗(請求で却下された注文、不正な商品コード等)。ビジネス側に既に対応手順が存在する | 型で表現してドメインに含める |
| パニック | システムを未知の状態に置くもの | 落とす/握らない |
| インフラエラー | ネットワーク断、DB 接続失敗など | 型では追わず一括で扱う |
同書のテイクアウェイは「エラーをドメインの一部にする」「シグネチャの正直さ(Signature Honesty)」。例外を投げる代わりに直和型で失敗ケースを列挙する。
// Wlaschin の F# 例を TypeScript に置き換えたもの
type AddressValidationError =
| { type: "InvalidFormat"; detail: string }
| { type: "AddressNotFound"; detail: string };
type CheckAddressExists =
(addr: UnvalidatedAddress) => Result<CheckedAddress, AddressValidationError>;
ポイントは、AddressValidationError の各ケース名がユビキタス言語であること。これが「エラーがドメイン」の中身。
2. Effect 公式も同じ線引きを明文化している
Effect のドキュメントは expected errors / unexpected errors(defects)を分け、expected errors は「プログラムのドメインと制御フローを定義する役割を果たす」ものとして型レベル(Error チャンネル)で追跡すると明記している。defects は unchecked exception 同様、想定された振る舞いの外側。
ここまで来ると「エラー = ドメイン」はデファクトと言ってよい。
3. ただし「全部やる」は非現実的
エラー・例外が発生しうる箇所はアプリのいたるところにあり、そのすべてを個別の Result 型に変換するのは開発・保守コスト的に持たない。実務的な落とし所は「ハンドリングが必要なものだけ個別のエラー型にし、必要ないものは UnexpectedError にまとめる」。
判断基準は「呼び出し側でハンドリングしたいかどうか」。
| カスタムエラー | 個別型にするか | 理由 |
|---|---|---|
ValidationError | する | ユーザーにフィードバックして修正を促す必要がある |
HttpError | する | not found / 認証エラー等でユーザーへの提示が変わる |
NetworkError | しない | 具体的内容をユーザーに見せない |
ParseJsonError | しない | 同上 |
type GetPokemonError = ValidationError | HttpError | UnexpectedError;
const getPokemon = async (
name: string
): Promise<Result<Pokemon, GetPokemonError>> => {
try {
const v = validateName(name); // → ValidationError(ドメイン)
if (isFailure(v)) return v;
const res = await fetch(url(v.value)); // → 失敗は UnexpectedError
const c = checkHttpStatus(res); // → HttpError(ドメイン)
if (isFailure(c)) return c;
return succeed(await c.value.json()); // → 失敗は UnexpectedError
} catch (error) {
return fail(new UnexpectedError({ cause: error }));
}
};
これは実質「ドメインエラーだけドメイン型にする」の言い換えで、Wlaschin の 3 分類と一致する。
4. 「全部型にする」の古典的な反論 = 検査例外
Result 型でエラーを全部型安全にすると、Java の検査例外とほぼ同じ問題が出る。
- エラー型の変更がコードの互換性を壊す
- OCP(開放閉鎖原則)に違反する
- インターフェース/抽象クラスで指定したエラー型が具象側の発生エラーを制限してしまう
- エラー型が増えすぎてハンドリングしきれなくなる
→ 対象をドメインエラーに絞ることがそのまま緩和策になっている。
5. Error 継承の有無は直交する別軸
「ドメイン概念として扱うか」と「Error クラスを継承するか」は独立した論点。
継承しない派の根拠
try…catchスタイルはどうしても併用が必要なので、Errorはそちら用に取っておいて区別したほうが分かりやすい- 目印用の super class を作ってもいいが、わざわざ
Errorを継承する理由もない(値として合成したいだけなら素のクラス/オブジェクトで足りる)
継承する派の根拠
obj instanceof Errorで識別できる- Node.js / ブラウザでスタックトレースが取れる(原因特定に効く)
共通の注意点
- TypeScript では継承前提のコードを書くことは極めてまれ。例外クラスも
BaseErrorの直系の子クラスだけで十分で、Java 的な立派な継承ツリーの設計は不要。
現実解はハイブリッド。Effect の Data.TaggedError は「Error のサブクラスでありつつ _tag でタグ付きユニオンとして扱う」形を採っている。
// A. 継承しない(値としてのドメインエラー)
type OrderError =
| { readonly _tag: "OutOfStock"; readonly sku: string }
| { readonly _tag: "PaymentDeclined"; readonly reason: string };
// B. 継承する(スタックトレース / instanceof が欲しい)
class OutOfStockError extends Error {
readonly _tag = "OutOfStock" as const;
constructor(readonly sku: string) {
super(`out of stock: ${sku}`);
this.name = new.target.name;
}
}
A も B も「ドメインエラーとして扱っている」点は同じ。選択軸はスタックトレース / instanceof が欲しいか vs 値として合成したいかであって、ドメインかどうかではない。
6. 判定のチェックリスト
あるエラーをドメイン型として切り出すべきかは、次の 2 問で判定できる。
- ドメインエキスパートがこの失敗に名前を持っているか?(「在庫切れ」「与信否決」→ Yes /「JSON パース失敗」→ No)
- 呼び出し側が種類ごとに違う対応をするか?(Yes なら個別型、No なら
UnexpectedErrorに集約)
補助的な材料として、集約の不変条件違反はドメインエラーの典型例とされる(ビジネスオブジェクトがドメイン視点で不正な状態にあることを意味するため)。
まとめ
エラーをドメイン概念として扱うのは普通どころか関数型 DDD の前提。ただし「ドメインエラーだけ」が対象で、Error 継承の有無はそれとは別軸の実装判断。
参考
- Scott Wlaschin『Domain Modeling Made Functional』"Working with Domain Errors" — https://www.oreilly.com/library/view/domain-modeling-made/9781680505481/f_0098.xhtml
- Domain Modeling Made Functional: Takeaways(Make errors part of your domain) — https://canro91.github.io/2021/12/13/DomainModelingMadeFunctional/
- Effect Documentation "Two Types of Errors" — https://effect.website/docs/error-management/two-error-types/
- ゲントク「TypeScript に Result 型を導入するための妥協点はどこか?」 — https://zenn.dev/praha/articles/2eb151a891be16
- motojouya「TypeScript のエラーハンドリングまとめ」 — https://zenn.dev/motojouya/articles/typescript_error_handling
- frozenbonito「TypeScript のエラーハンドリングを考える」 — https://qiita.com/frozenbonito/items/e708dfb3ab7c1fd3824d
- 「例外処理」仕事ですぐに使える TypeScript — https://future-architect.github.io/typescript-guide/exception.html
- 「カスタムエラー, Error の拡張」現代の JavaScript チュートリアル — https://ja.javascript.info/custom-errors
- Sansan Tech Blog「TypeScript 開発に Railway Oriented を持ち込み、より型安全なエラーハンドリングへ」 — https://buildersbox.corp-sansan.com/entry/2024/03/26/110000