制御なき例外はシステムを殺す:VB.NETにおける堅牢なエラーハンドリングとログの極意
現場のエンジニア諸君。あなたがメンテナンスしているそのVB.NETアプリケーションは、明日、いや数分後にも予期せぬクラッシュを起こすリスクを抱えていないか?
「とりあえず `Try-Catch` で囲んで、メッセージボックスを出しておけばいい」
もしそのような甘い認識でコードを書いているとしたら、今すぐその手を止め、このアーキテクチャ論に耳を傾けてほしい。
レガシーなVBAシステムからの移行組、あるいはWindowsフォーム全盛期から引き継がれる負債を抱えた現場において、例外処理の甘さは致命的なデータ破損やリソースリークを引き起こす。特に、アンマネージド資源を叩くWindows APIの呼び出しや、基幹システム間連携におけるネットワーク断など、現実の荒波に揉まれるシステムにおいて、「正しく例外を捉え、確実に始末をつける」ことはエンジニアの生娘の様なプライド以前の、生存戦略そのものだ。
今回は、VB.NETの例外処理(`Try-Catch-Finally`)の真髄と、プロフェッショナルが実装すべきログ出力の基本を、容赦ない実務コードと共に叩き込む。
—
1. 初心者が陥る「悪魔のアンチパターン」
まず、多くの初学者、あるいは場当たり的なコーディングを行う者が犯す最大の罪から指摘しよう。
.net
‘ 【絶対にやってはならない最悪のコード】
Try
‘ 何らかの危険な処理
Call SomeMethod()
Catch ex As Exception
‘ 何もしない(あるいはメッセージ出して終わり)
‘ ログも残さない
End Try
これはいわゆる「例外の握りつぶし(Swallowing Exceptions)」だ。エラーが発生した事実を隠蔽し、プログラムを無理やり続行させる。結果として、後続の処理でより深刻なデータ破壊を引き起こし、原因特定を不可能にする。エラーを隠すコードは、バグそのものよりもタチが悪い。
また、無駄に広い範囲を `Try` で囲むこともパフォーマンスとデバッグ性を著しく低下させる。例外が発生しうる最小限のスコープに絞るのが、一流のエンジニアの流儀である。
—
2. 【実践】`Try-Catch-Finally` の正しい構造とリソースの死守
例外が発生しようとも、発生しまいとも、必ず実行しなければならない処理がある。それが「リソースの解放」だ。
特に、ファイルストリーム、データベースのコネクション、あるいはCOMオブジェクトやWindows APIが絡むハンドル操作において、メモリリークやハンドル枯渇はシステム全体の死を意味する。
ここで `Finally` ブロック、そして .NET が提供する `Using` 構文の真価を発揮させる。
.net
Imports System.IO
Imports System.Runtime.InteropServices
Public Class RobustDataProcessor
‘ Windows API連携の例(メモリ・ハンドル管理の厳格化)
Private Shared Function Beep(dwFreq As UInteger, dwDuration As UInteger) As Boolean
End Function
”’
”’
Public Sub ProcessData(ByVal filePath As String)
Dim fs As FileStream = Nothing
Try
‘ 1. リソースの取得
fs = New FileStream(filePath, FileMode.Open, FileAccess.Read)
Using reader As New StreamReader(fs)
‘ ※ StreamReaderにOwnershipを持たせるため、fs自体のCloseに注意
Dim content As String = reader.ReadToEnd()
‘ 業務ロジックの実行
Me.ExecuteBusinessLogic(content)
End Using
Catch ex As FileNotFoundException
‘ 予想される特定の例外を先ず捕捉する
Logger.Error($”指定されたファイルが見つかりません: {filePath}”, ex)
Throw ‘ スタックトレースを維持したまま上位へ再スロー(安易なCatch and Throwは厳禁だが、ログ目的や境界でのスローは適切に行う)
Catch ex As Exception
‘ 予期せぬ例外の捕捉
Logger.Fatal($”予期せぬ致命的エラーが発生しました。処理を中断します。”, ex)
‘ ここでアプリケーションを安全に落とすための処理を入れる
Throw
Finally
‘ 2. アンマネージド資源や未開放ストリームの確実な解放
‘ Finallyブロックは、Return文が途中で実行されようとも必ず通過する
If fs IsNot Nothing Then
fs.Dispose()
fs = Nothing
End If
‘ Windows APIの安全な呼び出しテスト
Beep(800, 200)
End Try
End Sub
Private Sub ExecuteBusinessLogic(data As String)
If String.IsNullOrEmpty(data) Then
Throw New InvalidOperationException(“データが空です。”)
End If
End Sub
End Class
チーフアーキテクトの視点:`Finally` の不可侵性
`Finally` ブロックは、`Try` や `Catch` の中で `Return` が呼ばれたとしても必ず実行される。
メモリ管理の鉄則は「取得した者責任(Acquisition is allocation)」だ。自分で確保したファイルストリームやCOMオブジェクトは、例外の有無に関わらず、この `Finally` で確実に `Dispose` または `ReleaseComObject` を叩くこと。これをサボるエンジニアに、次のモジュールを任せるわけにはいかない。
—
3. ログ出力の基本:未来の自分と運用者を救う「文脈」の残し方
「エラーが発生しました」だけのログは、ゴミと同義である。
プロフェッショナルなログには、以下の「4つのW」が揃っていなければならない。
1. When(いつ): ミリ秒単位のタイムスタンプ
2. Where(どこで): クラス名、メソッド名、行番号(可能であれば)
3. What(何が起きたか): 例外メッセージ、スタックトレース、内部例外(InnerException)
4. Who / Context(どのような状態で): 処理対象のID、ユーザー、パラメータの状態
VB.NETにおける堅牢なロギング実装の骨組みを見てみよう(実務では NLog や Serilog などの成熟したフレームワークを使うべきだが、ここではラッパーの概念を示す)。
.net
Imports System.Diagnostics
Imports System.IO
Public NotInheritable Class Logger
Private Sub New()
‘ 静的クラスとしての実装
End Sub
Private Shared ReadOnly LogFilePath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “logs”, “system_error.log”)
”’
”’
Public Shared Sub Fatal(message As String, ex As Exception)
WriteLog(“FATAL”, message, ex)
End Sub
”’
”’
Public Shared Sub Error(message As String, ex As Exception)
WriteLog(“ERROR”, message, ex)
End Sub
Private Shared Sub WriteLog(level As String, message As String, ex As Exception)
‘ スレッドセーフティのための排他制御(必要に応じてMonitorやMutexを使用)
SyncLock GetType(Logger)
Try
Dim logDir As String = Path.GetDirectoryName(LogFilePath)
If Not Directory.Exists(logDir) Then
Directory.CreateDirectory(logDir)
End If
Using writer As New StreamWriter(LogFilePath, append:=True)
writer.WriteLine(“————————————————–“)
writer.WriteLine($”[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}]”)
writer.WriteLine($”Message: {message}”)
If ex IsNot Nothing Then
writer.WriteLine($”ExceptionType: {ex.GetType().FullName}”)
writer.WriteLine($”ExMessage: {ex.Message}”)
writer.WriteLine($”StackTrace: {ex.StackTrace}”)
‘ VB.NET/.NETの強力な武器:InnerExceptionの深掘り
‘ データベースやAPI連携では、真の原因はInnerExceptionの中に眠っていることが多い
Dim inner As Exception = ex.InnerException
Dim depth As Integer = 1
While inner IsNot Nothing
writer.WriteLine($”— InnerException (Depth: {depth}) —“)
writer.WriteLine($”Type: {inner.GetType().FullName}”)
writer.WriteLine($”Message: {inner.Message}”)
writer.WriteLine($”StackTrace: {inner.StackTrace}”)
inner = inner.InnerException
depth += 1
End While
End If
End Using
Catch logEx As Exception
‘ ログ出力自体の失敗でアプリケーションをクラッシュさせないための最終防衛ライン
Trace.WriteLine($”[CRITICAL] ログの書き込みに失敗しました: {logEx.Message}”)
End Try
End SyncLock
End Sub
End Class
なぜ `InnerException` を再帰的に辿るのか?
特にシステム間連携やDBアクセス(ADO.NETやEntity Framework)において、表面上に現れる例外は単なる「ラッパー(例:`TargetInvocationException` や `SqlException` の外側)」であることが非常に多い。
真のエラー原因(ネットワークのタイムアウト、SQLの制約違反など)は、その奥底にある `InnerException` に隠されている。これを再帰的にすべて吐き出させないログシステムは、暗闇の中で懐中電灯を持たずに歩くようなものだ。
—
4. チーフアーキテクトからの最後のアドバイス
VB.NETは、その簡潔な文法ゆえに「誰でも書ける言語」と侮られがちだ。しかし、だからこそ、書く人間の技量がコードの寿命をモロに決定づける。
例外処理とログ出力は、単なる「エラー対策」ではない。それは「システムが自身の健康状態を雄弁に語るためのシステム言語」なのだ。
明日書くコードから、無意味な `Catch ex As Exception` を排除せよ。リソースを溺愛し、例外の根っこまでログに刻み込め。
プロフェッショナルたるもの、予測不能なエラーに怯えるのではなく、エラーを完全に掌握し、手玉にとる気概を持て。
健闘を祈る。
