実務中級者への警鐘:その「Try-Catch」、例外の命を奪っていないか?
開発現場でよく目にする光景がある。
.net
Try
‘ データベース更新処理やファイルI/O
DataUpdate()
Catch ex As Exception
‘ ログに出して終わり、あるいは握りつぶす
MessageBox.Show(“エラーが発生しました: ” & ex.Message)
End Try
このコードを書いているうちは、あなたの作る業務アプリケーションは永遠に「原因不明のバグ」に怯え続けることになる。
マルチレイヤー(UI、ビジネスロジック、データアクセス)で構成される業務システムにおいて、エラーハンドリングの美しさはシステムの寿命を左右する。画面層で `ex.Message` だけを表示させてログに保存した気になっているエンジニアは、いざ本番環境で障害が起きたとき、「どのレイヤーの、どのメソッドの、何行目で起きたか」という最も重要な手掛かりを自らの手でゴミ箱に捨てていることに気づいていない。
今回は、VB.NETにおける `Exception.StackTrace` と `InnerException` を完全に手なづけ、レイヤーを越えてエラーを正確に追跡・伝播させるための「極限の例外設計」を伝授する。
—
1. なぜ「雑なCatch」が実務で地獄を生むのか?
業務アプリケーションは基本的に層(レイヤー)に分かれている。
1. UI層 (Presentation Layer): ユーザーからの入力を受け付け、結果を表示する。
2. BLL層 (Business Logic Layer): 業務ルール、計算、トランザクション制御を行う。
3. DAL層 (Data Access Layer): SQL Serverやファイルサーバー等への実アクセスを行う。
ここでDAL層でデータベース接続エラー(例: `SqlException`)が発生したとする。それをBLL層やUI層で以下のようにキャッチしてしまうとどうなるか。
.net
‘ ❌ 最悪なアンチパターン
Catch ex As Exception
Throw New Exception(“処理に失敗しました。”)
End Try
これでは、元の `SqlException` が持っていた「タイムアウトだったのか」「一意制約違反だったのか」という例外の本質(StackTraceとException Type)が完全に消失する。UI層に届くのは、中身のスカスカな「処理に失敗しました。」という汎用例外だけだ。これではデバッグしようがない。
—
2. 例外チェーン(InnerException)の正しい召喚
元の例外を隠蔽せず、上位レイヤーへ正確に文脈(Context)を付加して伝えるためには、例外のラップ(Exception Chaining)を行わなければならない。
VB.NETでは、`New CustomException(Message, InnerException)` のコンストラクタ構文を使用することで、下位層の例外を抱え込んだまま、上位層へバトンタッチできる。
究極のプロダクションコード例
以下のコードは、DAL層からBLL層、そしてUI層へと、例外情報とスタックトレースを完璧に維持したまま伝播させるマルチレイヤーのエラーハンドリング実装例だ。
.net
Imports System
Imports System.IO
Namespace EnterpriseApp.DataAcquisition
‘ ==========================================
‘ DAL (データアクセス層)
‘ ==========================================
Public Class CsvRepository
Public Sub ReadData(filePath As String)
Try
‘ 存在しないファイルへのアクセスを想定した処理
Using reader As New StreamReader(filePath)
Dim line As String = reader.ReadLine()
‘ 業務処理…
End Using
Catch ex As FileNotFoundException
‘ 【重要】DAL層では、低レベルの例外をキャッチし、固有のデータアクセス例外としてラップする
‘ 元の例外(ex)を InnerException として必ず渡すこと!
Throw New DataAccessException($”指定されたデータファイルが見つかりません: {filePath}”, ex)
Catch ex As Exception
‘ 予期せぬシステム例外のラップ
Throw New DataAccessException(“データ読み込み中に予期せぬハードウェア/OSエラーが発生しました。”, ex)
End Try
End Sub
End Class
End Namespace
Namespace EnterpriseApp.BusinessLogic
Imports EnterpriseApp.DataAcquisition
‘ ==========================================
‘ BLL (ビジネスロジック層)
‘ ==========================================
Public Class OrderManager
Private ReadOnly _repository As New CsvRepository()
uPublic Sub ProcessMonthlyOrder(filePath As String)
Try
_repository.ReadData(filePath)
Catch ex As DataAccessException
‘ 【重要】BLL層では、業務上のコンテキスト(どの月の処理でコケたか等)を付加して再スローする
‘ ここでも InnerException に ex を指定し、チェインを絶対に切らない
Throw New BusinessLogicException(“月次受注データの処理に失敗しました。”, ex)
End Catch
End Sub
End Class
End Namespace
Namespace EnterpriseApp.Presentation
Imports EnterpriseApp.BusinessLogic
Imports EnterpriseApp.DataAcquisition
‘ ==========================================
‘ UI層 (プレゼンテーション層 / エントリポイント)
‘ ==========================================
Public Class MainForm
Private Sub btnExecute_Click(sender As Object, e As EventArgs) Handles btnExecute.Click
Dim manager As New OrderManager()
Try
manager.ProcessMonthlyOrder(“C:\Data\MissingFile.csv”)
Catch ex As Exception
‘ 最上位のUI層で一元的にログ出力とユーザー通知を行う
Dim detailedLog As String = FormatExceptionDetails(ex)
‘ ログファイルやDBへ出力(本格的なロギングライブラリ NLog / log4net 等を推奨)
System.IO.File.WriteAllText(“C:\Logs\Error.log”, detailedLog)
‘ ユーザーには優しく、しかし簡潔にエラーを通知
MessageBox.Show(ex.Message, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
”’
”’
Private Function FormatExceptionDetails(ex As Exception) As String
Dim sb As New System.Text.StringBuilder()
Dim depth As Integer = 0
Dim current As Exception = ex
While current IsNot Nothing
sb.AppendLine($”— [層 / 深度: {depth}] —“)
sb.AppendLine($”例外型: {current.GetType().FullName}”)
sb.AppendLine($”メッセージ: {current.Message}”)
sb.AppendLine($”スタックトレース:”)
sb.AppendLine(current.StackTrace)
sb.AppendLine()
‘ 次の内部例外(InnerException)へ潜る
current = current.InnerException
depth += 1
End While
Return sb.ToString()
End Function
End Class
‘ ==========================================
‘ カスタム例外クラス群
‘ ==========================================
Public Class DataAccessException
Inherits Exception
Public Sub New(message As String, innerException As Exception)
MyBase.New(message, innerException)
End Sub
End Class
Public Class BusinessLogicException
Inherits Exception
Public Sub New(message As String, innerException As Exception)
MyBase.New(message, innerException)
End Sub
End Class
End Namespace
—
3. この設計がもたらす圧倒的なアドバンテージ
上記のコードを実装したアプリケーションでエラーが発生した場合、出力されるログファイルには以下のような極めて価値の高い情報が残る。
— [層 / 深度: 0] —
例外型: EnterpriseApp.Presentation.BusinessLogicException
メッセージ: 月次受注データの処理に失敗しました。
スタックトレース:
場所 EnterpriseApp.BusinessLogic.OrderManager.ProcessMonthlyOrder(String filePath)
場所 EnterpriseApp.Presentation.MainForm.btnExecute_Click(…)
— [層 / 深度: 1] —
例外型: EnterpriseApp.DataAcquisition.DataAccessException
メッセージ: 指定されたデータファイルが見つかりません: C:\Data\MissingFile.csv
スタックトレース:
場所 EnterpriseApp.DataAcquisition.CsvRepository.ReadData(String filePath)
— [層 / 深度: 2] —
例外型: System.IO.FileNotFoundException
メッセージ: ファイル ‘C:\Data\MissingFile.csv’ が見つかりませんでした。
スタックトレース:
場所 System.IO.StreamReader..ctor(String path, …)
…
開発者はログを見るだけで、「UI層のどのボタンから始まり、BLLを経て、DALのどのファイル読み込み(更には .NET Framework 内部の `StreamReader` のどこ)で起きたのか」を一目で把握できる。これこそが、プロフェッショナルなエラーハンドリングの姿だ。
—
4. 実務運用における重要な注意点
① `Throw` と `Throw ex` の違いを絶対に間違えるな
VB.NET(およびC#)において、Catchブロック内で例外を再スローする際、絶対にやってはいけない禁忌がある。
- `Throw ex` (やってはいけない)
- これを実行すると、その瞬間にスタックトレースがリセットされ、「例外を再スローした行」がスタックトレースの起点になってしまう。元のエラー発生源の行番号が失われるため、絶対に書いてはならない。
- `Throw` (正しい)
- 現在キャッチしている例外オブジェクトとそのスタックトレースを完全に保持したまま、そのまま上層へスローし直す。
② パフォーマンスとセキュリティのトレードオフ
スタックトレースや内部例外の文字列結合、ファイルI/Oはコストが高い処理である。そのため、定常的な業務フロー(例:入力値チェックでユーザーがフォームに入力を間違えた等)で例外を制御フロー(Control Flow)の代わりにしてはならない。例外は「例外的なシステム異常」にのみ使うこと。 業務上のバリデーションエラーは、例外を投げずに `Boolean` や結果オブジェクトを返す設計にすべきだ。
—
チーフアーキテクトからの最終提言
「動けばいい」というコードは、開発者自身の首を絞めるブーメランだ。
あなたが構築する業務自動化ツールやエンタープライズアプリケーションが、将来的に何百人ものユーザーに使われ、深夜のトラブルシューティングに怯えることのないようにするために。今日から `Catch ex As Exception` の中身を見直し、InnerExceptionとStackTraceをチェーンさせる高潔な例外設計を導入してほしい。
コードの美しさは、エラーハンドリングの気高さに宿る。
