実装のベストプラクティスにはないが、言語ページの複数に同じ趣旨で書かれている原則を拾った。言語ごとの書き方は各節の出典にある。
目次
- エラーは包んで渡し、処理は1か所で
- 想定内の失敗は戻り値で表す
- 外部データは境界でスキーマに通す
- 呼び出し側で意味が読める引数にする
- 状態は不変を基本にし、外から渡す
- 「値がない」の表し方を決める
- 待つ処理には上限と止め方を付ける
- 設定は起動時に検証し、止まり方を決めておく
- 依存は少なく、ロックファイルで固定する
- 言語を問わず出てくるアンチパターン
1. エラーは包んで渡し、処理は1か所で
下の層は「何をしようとして失敗したか」を足して上へ渡し、ログを出すか応答に変えるのは1か所だけ
- 元のエラーは捨てずに原因として繋ぐ(Go の
%w、Python のraise ... from e、JS のcause) - ログに出してからさらに返すと、同じエラーが何度もログに出る。出すか返すか、どちらか一方
- 判定は型や識別子で行い、メッセージの文字列では比べない(文言を変えると壊れる)
- 独自のエラーは基底を1つ作って階層にすると、呼び出し側がまとめて捕まえられる
- 想定内の失敗(入力不正・外部の失敗)とバグを分ける。バグは無理に回復せず落として再起動させる
出典: Go / Python / Node.js / TypeScript
2. 想定内の失敗は戻り値で表す
例外は想定外のために取っておき、起きると分かっている失敗は戻り値の型で返す
- バリデーションエラーなどを戻り値(Result 型、Go の
error)で返すと、呼び出し側が扱うのを忘れられない - 投げるのはエラーの型だけにする。文字列などを投げるとスタックトレースが残らない
- 通常の失敗に panic や致命的な例外を使わない。呼び出し側が回復できなくなる
出典: TypeScript / Node.js / Go
3. 外部データは境界でスキーマに通す
型はコンパイル時にしかないので、外から来るものは境界で一度だけ実行時に検証し、内側は検証済みの型を受け取る
- 対象はリクエスト・外部 API の応答・キューのメッセージ・環境変数・
JSON.parseの結果など - スキーマから型を導くと、型とバリデーションを二重に書かずに済む(Zod、pydantic など)
- 未知の項目は落とすか拒否して、意図しない項目の一括代入を防ぐ
- 連想配列や辞書のまま奥へ渡さず、値オブジェクトや DTO にしてキーのタイポや型の不一致を防ぐ
出典: TypeScript / Node.js / PHP / Python
4. 呼び出し側で意味が読める引数にする
呼び出しの1行だけを読んで、何を渡しているか分かるようにする
- 真偽値のフラグ引数を避ける。
fetch(true)ではなく名前付きの選択肢にする - 省略できる設定はキーワード引数・オプションのオブジェクトで受ける
- 引数は広い型(読み取り専用など)で受け、戻り値は狭い型で返す
- 可変の値をデフォルト引数にしない(Python では呼び出し間で共有される)
出典: TypeScript / Python / Python
5. 状態は不変を基本にし、外から渡す
値は作ったら変えず、依存は中で作らずに外から注入する
- 変更が要るときは元を書き換えずに新しい値を返す(
readonly、frozen、final) - 継承は深くせず、小さな部品の組み合わせ(コンポジション)とインターフェースで広げる
- 可変なグローバル変数や static な状態を持たない。テストで差し替えられず、並行処理で競合する
- 時刻や乱数も依存として注入すると、テストで固定できる
出典: PHP / TypeScript / Python / Go
6. 「値がない」の表し方を決める
null(nil、None)を返しうる箇所を型や名前で明示し、チームで表し方を揃える
- 型で null を検査できる言語では有効にする(TypeScript の
strictNullChecks) - デフォルト値は「null のときだけ」置き換える演算子を使う。
||は0や空文字も置き換えてしまう - 安全な参照の演算子(
?.、&.)が長く連なるのは、設計を見直すサイン - 返す値がないときは Null Object など、呼び出し側が分岐しなくて済む形も検討する
出典: TypeScript / Ruby
7. 待つ処理には上限と止め方を付ける
外部への呼び出しにはタイムアウトを、起動した並行処理には終わらせ方を必ず用意する
- HTTP クライアントなどの既定値はタイムアウトなしのことがある。明示して設定する
- キャンセルを受け取れる形にする(Go の
context、JS のAbortSignal) - 起動した並行処理は終了を待つ。止められないものはリークになる
- 大量の並列実行は同時実行数を絞る。無制限だと接続やメモリが尽きる
- 投げっぱなしの非同期処理を作らない。待つか、失敗の扱いを明示する
出典: Go / Go / Node.js / TypeScript
8. 設定は起動時に検証し、止まり方を決めておく
設定は起動時に一度だけ読んで検証し、終了シグナルを受けたら片付けてから止まる
- 環境変数をコードのあちこちで直接読まず、型の付いた設定オブジェクトにまとめる
- 足りない・不正な値があれば起動時にすぐ失敗させる
- 終了シグナル(SIGTERM)を受けたら新規の受付を止め、処理中のものと接続を片付けてから終わる。片付けにも上限時間を設ける
9. 依存は少なく、ロックファイルで固定する
足す前に標準機能で足りないか確かめ、アプリケーションではロックファイルをコミットする
- 追加するときはメンテナンス状況・依存の大きさ・ライセンスを確かめる
- CI と本番はロックファイルどおりに入れる(
npm ci、uv syncなど) - ライブラリは依存のバージョン指定を緩めに、アプリケーションはロックファイルで厳密に固定する
- 更新は自動化して小さく頻繁に入れる
出典: Node.js / Python / PHP / Go
10. 言語を問わず出てくるアンチパターン
各言語ページのアンチパターン表のうち、複数の言語に出てきて、実装のページにはないもの。
| アンチパターン | 代わりに |
|---|---|
| エラーをログに出してから、さらに返す | 出すか返すか、どちらか一方にする |
| エラーメッセージの文字列で判定する | エラーの型や識別子で判定する |
util / common / Helper の何でも入れる箱 | 機能を表す名前の単位に分ける |
| 可変なグローバル変数 | 引数や依存性注入で渡す |
型の検査を黙らせる(any、as、@ts-ignore) | 型ガードやスキーマ検証で絞る |
| タイムアウトのない外部呼び出し | タイムアウトを明示する |
出典: Go / Python / PHP / TypeScript
関連
- 実装のベストプラクティス — このページの元になる原則とPR前のチェックリスト
- テストコードのベストプラクティス — 依存を注入して時刻や乱数を固定する側
- ログ設計 — エラーを1か所でログに出すときの形