null が危険なのは、それがどの型にも入り込むからです。文字列を受け取ったつもりの関数に null が渡り、メソッド呼び出しの瞬間に落ちる。トニー・ホーアが「10 億ドルの過ち」と呼んだのはこの性質のことです。

ただし現代の言語では、null そのものより扱い方が問題になります。Optional 型があっても、雑に unwrap すれば結果は同じです。ここでは「不在」を生成する側・伝播させる側・消費する側の 3 段階に分けて、注意点を整理します。

生成側: null を返す前に一度止まる

まず疑うべきは「そもそも不在を返す必要があるか」です。不要な null は呼び出し側全員にチェックを強制します。

状況 よくある実装 望ましい実装
該当件数 0 件の一覧 null を返す 空の配列を返す
設定値が未指定 null を返す デフォルト値を返す
集計対象が空 null を返す ゼロ値・単位元を返す
ID 指定の取得で不在 null を返す 不在が正常なら Optional、異常なら例外

コレクションを null で返すのは特に避けます。null と空配列は呼び出し側にとって「要素がない」という同じ意味しか持たないのに、書き方だけが分岐するからです。

// 呼び出し側に無意味な分岐を強いる
function findTags(postId: string): string[] | null

// 「該当なし」は空配列で十分に表現できる
function findTags(postId: string): string[]

その上で本当に不在を返すなら、不在の理由が 1 つに定まっているかを確認します。「見つからない」と「取得に失敗した」が同じ null に潰れているなら、それは型が足りていない合図です。

伝播側: 不在の生存範囲を狭くする

null は放っておくとコードベース全体に広がります。外部から入ってきた不在を、そのまま内部へ流さないことが肝心です。

原則は境界で正規化する。API レスポンス、DB の NULL 許容カラム、環境変数——外部由来の値は境界のパース処理で「不在なし」の型に変換し、内部のドメインモデルには持ち込みません。

// 境界: 外の世界の欠損をここで吸収する
struct RawConfig {
    timeout_ms: Option<u64>,
}

// 内部: 不在が存在しない型
struct Config {
    timeout: Duration,
}

impl Config {
    fn from_raw(raw: RawConfig) -> Self {
        Config {
            timeout: Duration::from_millis(raw.timeout_ms.unwrap_or(3000)),
        }
    }
}

こうしておくと、Config を受け取る側は不在を一切考えなくて済みます。逆に境界での吸収を怠ると、Option が構造体の奥深くまで伝播し、参照するたびに「これは本当に無いことがあるのか」を調べる羽目になります。

内部に残す Option は「業務的に本当に任意である」もの——たとえば記事のサブタイトル——だけに絞ります。技術的な事情による不在をドメイン型に混ぜない、と言い換えられます。

消費側: チェックの形を揃える

不在を受け取ったら、扱い方は 3 つしかありません。

  1. デフォルトで埋める??unwrap_or
  2. 不在のまま次へ渡す?.mapand_then
  3. エラーに変換するok_or、例外送出)

問題は、この 3 つが混ざった深いネストです。

// ネストが深く、どの分岐がどの不在に対応するか読めない
if (user !== null) {
  if (user.profile !== null) {
    if (user.profile.avatar !== null) {
      return user.profile.avatar.url;
    }
  }
}
return DEFAULT_AVATAR;

// 早期リターンかオプショナルチェーンで平坦にする
return user?.profile?.avatar?.url ?? DEFAULT_AVATAR;

ここで注意したいのが、??unwrap_or による握りつぶしです。デフォルト値で埋めると処理は進みますが、「不在が起きたこと」は消えます。ユーザー ID が取れないのにゲスト扱いで進む、金額が取れないのに 0 円で計上する——落ちないぶん発見が遅れます。

デフォルト値で埋めてよいのは、不在が正常系の一部であるときだけです。判断に迷ったら「この値が無いまま処理が完了したとき、それは成功と呼べるか」を問います。呼べないなら埋めずにエラーへ変換します。

非 null 表明は最後の手段

TypeScript の !、Rust の unwrap()、Kotlin の !! は、型検査を黙らせる道具です。使う場面はあります。「直前で存在を確認した」「定数から構築したので失敗しえない」といったケースです。

ただし条件があります。なぜ安全なのかをその場に書き残すこと

// 直前の insert で必ず存在するため取得は失敗しない
let entry = map.get(&key).expect("just inserted above");

expect のメッセージは、失敗時のログのためというより、読み手への根拠の提示として機能します。根拠が書けないなら、それは表明ではなく賭けです。

null が意味の運び屋になっていないか

最後に、設計上いちばん厄介な使い方を挙げておきます。null業務的な意味を持たせるパターンです。

  • deletedAtnull なら未削除
  • approvedBynull なら未承認
  • endDatenull なら無期限

どれも実務でよく見ますし、それ自体が即座に誤りというわけではありません。危険なのは、こうしたフィールドが増えて組み合わせの整合性が型に現れなくなったときです。「削除済みなのに承認者がいる」状態がコンパイルを通ってしまいます。

このときは null の使い方を直すのではなく、状態そのものを型で表現し直すのが筋です。列挙で状態を切り、各状態が持つデータだけを内側に置きます。null の問題に見えて、実際には型設計の問題であることは珍しくありません。

まとめ

  • 不在を返す前に、空コレクション・デフォルト値・例外で代替できないか検討する
  • コレクションは null ではなく空を返す
  • 外部由来の不在は境界で吸収し、ドメイン型には持ち込まない
  • 内部に残す Optional は「業務的に任意」なものだけに絞る
  • 深いチェックのネストは早期リターンかチェーンで平坦にする
  • デフォルト値で埋めてよいのは、不在が正常系のときだけ
  • 非 null 表明を使うなら、安全である根拠をその場に書く
  • null に業務的な意味を持たせ始めたら、状態を型で表し直す合図