【実務・中級編】初心者向け:VB.NETの例外処理(Try-Catch-Finally)の正しい書き方とログ出力の基本 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【VB.NET】例外処理(Try-Catch-Finally)の鉄則とログ出力:現場で絶対に落とさない業務自動化ツールの堅牢設計

「自分のPCでは動いたのに、現場のユーザーが使うと黙って強制終了する」
「エラーが出ているが、ログが何も残っていないため原因調査に何日も費やした」

業務効率化ツールや社内自動化システムをVB.NETで構築する際、このような惨劇に直面したことはないでしょうか。

現場で信頼されるツールと、素人が作ったバグまみれのツールの差は「エラーが起きたときにどう振る舞うか」、すなわち例外処理とログ設計の質で決まります。

本記事では、VB.NETにおける正しい例外処理(Try-Catch-Finally)の書き方、やってはいけないアンチパターン、そして現場で即投入できる「ログ出力機構を備えた堅牢なプロダクションコード」までを徹底解説します。

1. 絶滅させるべき3つの「悪魔の書き方」(アンチパターン)

まずは、プロの開発現場で即刻リファクタリング対象となる絶対NGなコードを知ってください。これらを放置することは、将来のシステム障害を放置することと同義です。

1-1. `On Error Resume Next` の遺物

‘ ❌ 絶対に書いてはいけない(VBA/VB6時代の名残り)
On Error Resume Next
Dim text As String = File.ReadAllText(“C:\non_existent_file.txt”)
‘ エラーを無視して後続処理が実行され、予期せぬ二次災害(データ破壊等)を引き起こす

`.NET`の時代において、`On Error Resume Next` は毒でしかありません。エラーを無視して処理を続行させるため、データ不整合や原因不明のバグを引き起こす最大の要因になります。

1-2. 例外の「握りつぶし」(Swallowing Exceptions)

‘ ❌ 最悪のアンチパターン:エラーが起きたことすら闇に葬り去る
Try
ProcessDatabaseUpdate()
Catch ex As Exception
‘ 何もしない、または MsgBox(“エラーが発生しました”) だけ表示して終わる
End Try

例外を捕捉(Catch)しておきながら、ログも出さずに処理を揉み消すコードです。障害が発生しても誰も気づけず、後から「データベースのデータが更新されていない」という重大なトラブルに発展します。

1-3. スタックトレースを破壊する `Throw ex`

‘ ❌ スタックトレース(どこでエラーが起きたかの履歴)を書き換えてしまう
Try
ExecuteProcess()
Catch ex As Exception
Logger.Error(ex.Message)
Throw ex ‘ ← これにより、「エラーが発生した本当の行番号」が上書きされて消える!
End Try

例外を上位の処理へ再投げ(Rethrow)する際、`Throw ex` と書いてはいけません。例外が発生した真の行番号情報(スタックトレース)が破棄され、この `Throw ex` を書いた行がエラー発生元として上書きされてしまいます。正しくは単に `Throw` と書きます。

2. 堅牢な例外処理(Try-Catch-Finally)の基本構造

VB.NETにおける例外処理は、「具体的な例外から拾う」「リソースは確実に解放する」の2点が鉄則です。

2-1. 特定の例外を先頭で捕まえる(Catchの順序)

例外は「より具体的な派生クラス」から順番に捕獲し、最後に汎用的な `Exception` で受けます。

Try
Dim content As String = File.ReadAllText(filePath)
Catch ex As FileNotFoundException
‘ ファイルが存在しない場合の固有処理
Logger.Warn($”指定されたファイルが見つかりません: {filePath}”)
Catch ex As UnauthorizedAccessException
‘ アクセス権限がない場合の固有処理
Logger.Error($”ファイルへのアクセス権限がありません: {filePath}”)
Catch ex As Exception
‘ 上記以外の予期せぬエラー(バグやシステム障害)
Logger.Error(ex, “予期せぬエラーが発生しました。”)
Throw ‘ 上位へ伝播させる
End Try

2-2. `When` 句による例外フィルタリング(VB.NETの強力な機能)

VB.NETには、例外の種類だけでなく「例外の条件(内容)」でCatchするかどうかを判断する `When` 句が存在します。

Catch ex As IOException When ex.Message.Contains(“別のプロセスで使用中”)
‘ ファイルがロックされている場合のみここに突入する
Logger.Warn(“ファイルが他プロセスでロックされています。再試行します。”)

2-3. リソース解放は `Finally` ではなく `Using` ステートメントを使え

ファイル記述子(Stream)やデータベース接続(SqlConnection)など、アンマネージドリソースを扱う場合、`Try-Finally` で手動で `.Close()` や `.Dispose()` を呼ぶのは時代遅れであり、漏れの原因になります。

.NETでは `Using` ステートメント を使用すれば、例外が発生しようが正常終了しようが、ブロックを抜けた瞬間に決定論的にリソースが破棄(Dispose)されます。

‘ ⭕️ 最高に安全なリソース解放パターン(Try-CatchとUsingの融合)
Try
Using conn As New SqlConnection(connectionString)
conn.Open()
‘ データベース操作…
End Using ‘ ここで例外の有無にかかわらず確実に Close / Dispose される
Catch ex As SqlException
Logger.Error(ex, “DB処理中にエラーが発生しました。”)
End Try

3. 実務で勝つための「ログ出力設計」

エラーが発生した際、エンジニアが欲しい情報は次の4つだけです。

1. いつ発生したか(日時)
2. どの場所で発生したか(クラス名・メソッド名・行番号)
3. 何が起きたか(エラーメッセージ)
4. どのような経路で起きたか(スタックトレース・InnerException)

これらを漏れなくテキストファイル等に出力するログ機能を設計します。
※ログ出力処理自体が例外を起こしてアプリをクラッシュさせないよう、ロガー内部も強固に設計する必要があります(フェイルセーフ)。

4. コピペで使える!プロダクショングレードの実装例

以下は、業務自動化ツール(ファイル読み込み&データ処理)でそのまま活用できる、実務レベルの完全なモジュールコード例です。

実装コード(Logger & 業務ロジッククラス)

Imports System.IO
Imports System.Text

”’

”’ 簡易スレッドセーフ・フェイルセーフ・ロガー
”’ 本番環境ではNLogやSerilog等のライブラリ使用を推奨するが、
”’ 標準機能のみで構築する場合の最高峰実装。
”’

Public Class FileLogger
Private Shared ReadOnly LockObj As New Object()
Private Shared ReadOnly LogFolderPath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “Logs”)

”’

”’ ログメッセージを出力します。
”’

”’ ログレベル (INFO, WARN, ERROR) ”’ メッセージ ”’ 例外オブジェクト(任意) Public Shared Sub WriteLog(level As String, message As String, Optional ex As Exception = Nothing)
Try
‘ ログディレクトリの存在チェックと作成
If Not Directory.Exists(LogFolderPath) Then
Directory.CreateDirectory(LogFolderPath)
End If

Dim logFileName As String = $”App_{DateTime.Now:yyyyMMdd}.log”
Dim logFilePath As String = Path.Combine(LogFolderPath, logFileName)

‘ ログフォーマットの構築
Dim sb As New StringBuilder()
sb.AppendLine($”[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}”)

‘ 例外情報が存在する場合は詳細を出力
If ex IsNot Nothing Then
sb.AppendLine($” [ExceptionType] {ex.GetType().FullName}”)
sb.AppendLine($” [ExceptionMessage] {ex.Message}”)
sb.AppendLine($” [StackTrace]{Environment.NewLine}{ex.StackTrace}”)

‘ 内部例外(InnerException)が存在する場合は再帰的に出力
Dim innerEx As Exception = ex.InnerException
While innerEx IsNot Nothing
sb.AppendLine($” [InnerException] {innerEx.GetType().FullName}: {innerEx.Message}”)
innerEx = innerEx.InnerException
End While
End If

‘ 排他制御を行いつつ追記(複数スレッドからの同時書き込み対策)
SyncLock LockObj
File.AppendAllText(logFilePath, sb.ToString(), Encoding.UTF8)
End SyncLock

Catch loggerEx As Exception
‘ ロガー自体が失敗した場合は、最悪コンソール等に逃がし、メイン処理を巻き込んで落ちるのを防ぐ
System.Diagnostics.Trace.WriteLine($”ロガー内でエラー発生: {loggerEx.Message}”)
End Try
End Sub

Public Shared Sub Info(msg As String)
WriteLog(“INFO”, msg)
End Sub

Public Shared Sub Warn(msg As String)
WriteLog(“WARN”, msg)
End Sub

Public Shared Sub [Error](msg As String, Optional ex As Exception = Nothing)
WriteLog(“ERROR”, msg, ex)
End Sub
End Class

”’

”’ 業務自動化ロジックの実装例
”’

Public Class DataProcessor

”’

”’ CSVファイルを読み込んで安全に処理を実行します。
”’

”’ 対象ファイルパス Public Sub ProcessBusinessData(filePath As String)
FileLogger.Info($”処理を開始します。対象ファイル: {filePath}”)

‘ 事前チェック(ガード節):例外を発生させる前に検証できるものは検証する
If Not File.Exists(filePath) Then
FileLogger.Warn($”処理を中断しました。ファイルが存在しません: {filePath}”)
Return
End If

Try
‘ I/Oリソースは Using で確実に安全に解放する
Using reader As New StreamReader(filePath, Encoding.UTF8)
Dim lineNumber As Integer = 0

While Not reader.EndOfStream
lineNumber += 1
Dim line As String = reader.ReadLine()

Try
‘ 1行ごとの個別処理(特定行のエラーで全体を止めない設計)
ParseAndExecuteLine(line, lineNumber)

Catch ex As FormatException
‘ 特定行のフォーマットエラーはログを記録して次の行へ進む
FileLogger.Warn($”[{lineNumber}行目] データフォーマットが不正です。スキップします。内容: ‘{line}’ – Details: {ex.Message}”)

End Try
End While
End Using

FileLogger.Info(“すべてのデータ処理が正常に完了しました。”)

Catch ex As UnauthorizedAccessException
‘ 権限エラーの場合
FileLogger.Error($”ファイルへのアクセス権限がありません。ファイル属性やフォルダ権限を確認してください。 Path: {filePath}”, ex)
Throw ‘ 上位UI層(フォーム等)に知らせてユーザー表示するために伝播させる

Catch ex As IOException When ex.Message.Contains(“使用中”)
‘ 他プロセスによるファイルロックの場合(When句によるフィルタリング)
FileLogger.Error($”ファイルが他のアプリケーション(Excel等)で開かれています。ファイルを閉じて再実行してください。 Path: {filePath}”, ex)
Throw

Catch ex As Exception
‘ 想定外の致命的エラー
FileLogger.Error($”予期せぬシステムエラーが発生しました。処理を停止します。 Path: {filePath}”, ex)
‘ 正しい例外の再投げ(Throw ex はNG! Throw 単体でスタックトレースを維持)
Throw

Finally
‘ 後処理(必要に応じて一括テンポラリファイルの削除やステータスの復元を行う)
FileLogger.Info(“クリーンアップ処理を実行しました。”)
End Finally
End Sub

”’

”’ 行データのパースと実行(単一責任の原則に基づき分離)
”’

Private Sub ParseAndExecuteLine(line As String, lineNumber As Integer)
If String.IsNullOrWhiteSpace(line) Then Return

Dim cols As String() = line.Split(“,”c)

‘ 数値変換チェック(例外を投げる CInt ではなく TryParse を使うのがプロの鉄則)
Dim id As Integer
If Not Integer.TryParse(cols(0), id) Then
Throw New FormatException($”ID欄 ‘{cols(0)}’ を数値に変換できません。”)
End If

‘ ここで業務ロジックを実行…
End Sub

End Class

5. 現場のリーダーが伝える「堅牢設計の思考プロセス」

コードを書く際、以下のマインドセットを常に意識してください。

① 「Try-Catch」の前に「事前検証(ガード節)」を検討せよ

例外(Exception)の発生は、CPUのリソースを大量に消費する高コストな処理です。
「ファイルが存在しないかもしれない」「数値変換できないかもしれない」といった想定できる事態には、Try-Catchで捕まえるのではなく、`File.Exists()` や `Integer.TryParse()` などの事前判定(ガード節)を使って処理を分岐させるのが最速かつ最も綺麗な設計です。

② 例外を投げるときは「原因のコンテキスト」を付加せよ

標準の `Exception` をそのまま投げるのではなく、「どのファイルの何行目処理中だったか」というコンテキスト(文脈)情報をログや例外メッセージに含めてください。後でログを見たときの解析速度が10倍変わります。

③ ユーザーに見せる画面と、ログに残す詳細を分離せよ

一般ユーザー向けの画面(GUI)に、`NullReferenceException at Application.Main…` といった生々しいスタックトレースを表示させてはいけません。ユーザーには「システムエラーが発生しました。管理者にお問い合わせください」と優しく伝え、裏側のログファイルに技術的な詳細(スタックトレース含む)をフルで記録するのがプロダクションツールの嗜みです。

まとめ

1. `On Error Resume Next` や 空の Catch ブロックは絶対に排除する。
2. 例外の再投げは `Throw ex` ではなく `Throw` を使う。
3. ファイル・DB操作は `Try-Finally` ではなく `Using` ステートメントで確実に破棄する。
4. 例外処理の乱用に頼らず、可能な限りガード節(事前チェック)で回避する。
5. 発生日時・場所・内容・スタックトレースを含む堅牢なログ出力機構を組み込む。

適切な例外処理とログ設計が組み込まれたVB.NETプログラムは、運用コストを劇的に下げ、開発者であるあなた自身の信頼を跳ね上げます。明日のコードから、ぜひこの鉄則を適用してください。

タイトルとURLをコピーしました