Upgrade to Pro — share decks privately, control downloads, hide ads and more …

例外処理とどう使い分ける?Result型を使ったエラー設計 #burikaigi

例外処理とどう使い分ける?Result型を使ったエラー設計 #burikaigi

例外処理は、単なるコード上の仕組みではなく “失敗とどう向き合うか” を決める設計上の意思決定です。
エラー対応が「起きた後の対処」だけに偏ると、再発と手戻りは減りません。

Result型は、失敗の可能性を型で表し、例外に頼らずエラーを設計する手法です。
これにより、エラーの種類や処理責任が明確になり、設計の一貫性を保ちながら保守性を高められます。

本セッションでは、例外(try-catch)を用いる言語のプロジェクトにResult型を取り入れる設計方法を紹介します。
実務での知見を踏まえ、例外の扱いをより明確にし、エラー処理を改善するためのヒントと指針をお持ち帰りください!

BuriKaigi2026
https://fortee.jp/burikaigi-2026/proposal/1c532f7c-f089-4157-9dca-f4f235ebe52b

Avatar for Takuma Kajikawa

Takuma Kajikawa

January 10, 2026
Tweet

More Decks by Takuma Kajikawa

Other Decks in Programming

Transcript

  1. 梶川 琢馬 𝕏 @kajitack 所属: 株式会社 TechBowl 仕事: VPoT /

    TechTrain 開発 / メンター 前職: ゲーム / SaaS / EC / SNS 開発など 言語: PHP / TypeScript / Go 富山県高岡市出身 / 東京在住 BuriKaigiの参加は2回目! 前回は「例外処理を理解する」というテーマで登壇 5/60
  2. 前提と対象 前提 例外処理: try-catch 構文。および類似の構文 Result 型: 戻り値によって成功か失敗を表す。Either 型や Try

    型とも呼ばれる 対象 例外処理を持つ言語でのエラー設計について扱います。 なので、そもそも例外処理を持たない言語は対象外です。 7/60
  3. Result型の構造 構造はシンプル 成功すれば T 型の値、エラーの場合は E 型の値を持つ型 成功 ( Ok

    ): 正常な値を返す 失敗 ( Err ): エラー情報を返す enum Result<T, E> { Ok(T), Err(E), } 成功した場合、失敗した場合など処理を分岐できる 9/60
  4. 生成 成功 ( Ok ): 正常な値を返す 失敗 ( Err ):

    エラー情報を返す // 成功なら Ok(値) を返す if let Some(value) = input { return Ok(value); } // 失敗なら Err(エラー) を返す Err("Input cannot be null") 10/60
  5. 値として取り出す チェック用メソッド: is_ok() : 成功かどうかを判定 is_err() : 失敗かどうかを判定 値の取り出し: unwrap()

    : 成功時の値を 取り出す(失敗時はパニック) unwrap_err() : 失敗時のエラーを取り出す if result.is_ok() { let value = result.unwrap(); } else { let error = result.unwrap_err(); } 11/60
  6. 値として渡す Result 型を返す関数を合成する validate_user(user_id) .and_then(|user| check_stock(product_id, quantity)) .and_then(|_| process_payment(user_id, price))

    .and_then(|_| create_order(user_id, product_id, quantity)) .map(|order| order.id) // 注文IDに変換 .map_err(|e| format!("Order failed: {}", e)) .unwrap_or(OrderId::default()) 主要なメソッド: and_then : Result 型を返す関数を チェーン(失敗時は即座に伝播) map : 成功時の値を変換 (Result<T, E> → Result<U, E>) map_err : エラーの値を変換 (Result<T, E> → Result<T, F>) unwrap_or : 失敗時にデフォルト値を返す 12/60
  7. try-catchとの比較 try-catch function processUser(userId: string) { try { const user

    = findUser(userId); const email = user.email; sendMail(email); return "Success"; } catch (error) { console.error(error); return "Failed"; } } 各関数が例外を投げる可能性があるが、 型からは分からない Result 型 fn process_user(user_id: &str) -> Result<String, Error> { find_user(user_id) .and_then(|user| Ok(user.email)) .and_then(|email| send_mail(email)) .map(|_| "Success".to_string()) } 失敗の可能性が型で明示的 エラーハンドリング漏れはコンパイルエラー 14/60
  8. エラーハンドリングの歴史 フェーズ アプローチ 特徴 黎明期 正常値=0、それ以外の値はエラー エラー = 停止、またはステータスコード 戻り値のチェックが任意

    統一されてないので各メソッドで返す値がバラバラ エラー処理の体系化 例外(Exception) 大域脱出による分離、終了モデルの確立 Lisp, C++, Java, PHP, C# ... エラーは値 Result型、多値返却 成功/失敗を型で表現(Rust, Go, Haskell) コンパイラやLintで処理を強制、制御フローが明示的 例外処理が導入されることによってエラー処理が体系化された 参考 エラーハンドリングの歴史 https://faithandbrave.github.io/article/error_handling.html 例外を初めて実装した言語 https://yosuke-furukawa.hatenablog.com/entry/2021/12/24/145027 16/60
  9. 例外処理は「例外」のときのみ使う 制御フローに使うべきではない まとめてチェック try { const foo = runTask1(); const

    bar = runTask2(); } catch(e) { console.log('Error:', e); } 全部チェック try { const foo = runTask1(); } catch(e) { console.log('Error:', e); } try { const bar = runTask2(); } catch(e) { console.log('Error:', e); } https://typescript-jp.gitbook.io/deep-dive/type-system/exceptions 18/60
  10. ドメインロジック ビジネスプロセスの結果表現 -> Result 型 アプリケーション ドメイン層の Result 型を受け取り、 Presentation

    に適した例外を投げる ここで投げた例外はフレームワークの ハンドラまでキャッチしない 38/60
  11. ドメインモデルを常に正常に保つ 不正な状態を表現できないようにする / "Making illegal states unrepresentable" 無効な状態を型で表現できないようにし、失敗そのものが 起きない設計にする 想定外失敗は

    catch しない / "Let it crash" 想定内失敗と想定外失敗を分離、想定外は握りつぶさず 上位へ伝播させる 失敗を露出させることで原因特定が早く、監視と自動復旧で 運用として守る 境界で変換し、早く落とす / "Parse don't validate" / "Fail Fast" 境界で安全な型に変換して以後の分岐を消す 予防に勝る防御なし(2025年版) - 堅牢なコードを導く様々な設計のヒント https://speakerdeck.com/twada/growing-reliable-code-php-conference-fukuoka-2025 41/60
  12. 妥当性確認 妥当性確認には 2 種類ある 構文的(syntactical)な妥当性確認 ドメインモデルの状態に依存しない形式チェック → ValueObject やリクエストバリデーションで実施 意味的(semantical)な妥当性確認

    ドメインモデルの現在の状態を踏まえたビジネスルール → ドメイン層で実施 手を動かしてわかるクリーンアーキテクチャ ヘキサゴナルアーキテクチャによるクリーンなアプリケーション開発 42/60
  13. 構文的な妥当性確認の実装 数量 ValueObject を作成 コンストラクタが呼ばれた段階で処理を中断できる ドメインモデルの状態に依存しない構文的なチェック 呼び出し側の実装ミスなので、例外を投げる class Quantity {

    public function __construct(int $value) { if ($value <= 0) { throw new InvalidArgumentException('数量は1以上を指定してください quantity: '.$value); } $this->value = $value; } } 43/60
  14. Entityの作成をResult型で表現 失敗の種類を Enum で定義 enum OrderError { case ValidationError; case

    InsufficientStock; case PaymentFailed; case ShippingNotAvailable; } ワークフローで Result 型を活用 (Result 型は自作) /** * @return Result<Order, OrderError> */ public function create(OrderRequest $request): Result { } 46/60
  15. Entity作成に必要な処理の定義 Result 型を返す小さな関数に分割 function checkInventory(Order $order): Result { if (!hasStock($order))

    { return Result::err(OrderError::InsufficientStock); } return Result::ok($order); } // processPayment, scheduleShipping なども同様に定義 47/60
  16. 小さな関数を組み合わせてビジネスロジックを作成 andThen で成功した時だけ次の処理を実行する class OrderFactory { /** * @return Result<Order,

    OrderError> */ public function create( ProductId $productId, Quantity $quantity, CustomerId $customerId ): Result { return $this->validateOrder($productId, $quantity, $customerId) ->andThen(fn($data) => $this->checkInventory($data)) ->andThen(fn($data) => $this->processPayment($data)) ->andThen(fn($data) => $this->createOrder($data)); } } 48/60
  17. ユースケース側でResult型から例外への変換 Result 型のエラーを例外に変換し、フレームワークに委ねる $result = $this->orderFactory->create(...); if ($result->isErr()) { throw

    match ($result->unwrapErr()) { OrderError::InsufficientStock => new ConflictException(), OrderError::PaymentFailed => new PaymentRequiredException(), // ... }; } $order = $result->unwrap(); DB::transaction(fn() => $this->orderRepository->save($order)); return OrderDto::from($order); 50/60
  18. HTTPレスポンスの変換 ドメイン層内(Semantic Validation - 意味検証): ユーザー操作で回復可能 → 400 系 例:

    在庫切れ、重複登録 ドメインルール上定義されていない、捕捉できていないエラー → 500 系 ドメイン層の外での違反: Syntax Validation (形式検証) → 400 系 例: 必須パラメータ不足、型不一致 実装ミスや外部要因 → 500 系 例: DB 接続失敗、NullPointerException 51/60
  19. 実践した処理の流れをまとめると 入力層: 形式検証(構文チェック)→ validator ValueObject : コンストラクタで例外処理 不正な値を握りつぶさず、バグや境界の漏れを検出するため ドメイン層: 意味的検証(ドメインルール)→

    Result 型 インフラ層: 例外処理(DB 接続失敗、ネットワークタイムアウト) アプリケーション層: Result のエラーを例外処理として投げ直し、フレームワークに委ねる 52/60
  20. 使っている言語の特性を知ろう 値を表現出来るか どちらか一方の状態しか取り得ない(直和型)を実現するための機構 Tagged Union: Ok か Err のどちらか一方のみを持つ型 継承

    + ジェネリクス 値の取り出し 安全に取り出すための型ガードや静的解析によるチェックやパターンマッチに よって網羅できるか? 確認ポイント 代数的データ型をサポートしている?(直和型・直積型を組み合わせた型システム) 関数型スタイルを取り入れようとしている? もしくは将来的に実現する可能性があるのか? RFC や言語思想を確認してみるといいかも 57/60
  21. いろんな言語のエラー処理に ついて調べてみよう Go の Errors are values という考え方 Java の検査例外、非検査例外

    Swift のエラー4 分類 無効値の表し方 Nullable、Maybe、Optional... 設計のヒントになるかも? 58/60