テックカリキュラム

例外処理・ログ設計・障害解析

例外処理・ログ設計・障害解析

本章では、VB.NETにおける例外処理、ログ設計、障害解析について学習します。

業務システムは、正常に動作しているときだけを想定して作ればよいわけではありません。 入力ミス、DB接続エラー、ファイル読み込み失敗、ネットワーク障害、外部APIエラー、権限不足、想定外データなど、 実際の現場ではさまざまな異常が発生します。

そのため、VB.NETの業務アプリケーションでは、エラーをただ握りつぶすのではなく、 適切に検知し、利用者に分かりやすく通知し、開発者や運用担当者が原因を追跡できるようにする必要があります。

本章のゴールは、Try / Catchの構文を覚えることではありません。 業務例外とシステム例外を分け、ログを設計し、障害発生時に原因を追跡できる堅牢なアプリケーションを構築できるようになることです。


1. 例外処理の基本

例外とは、プログラムの実行中に発生する異常状態を表す仕組みです。 VB.NETでは、Try / Catch / Finallyを使って例外を処理します。

基本構文

Try
    ' 例外が発生する可能性のある処理

Catch ex As Exception
    ' 例外発生時の処理

Finally
    ' 例外の有無に関係なく必ず実行される処理

End Try

Tryブロックには、例外が発生する可能性のある処理を書きます。 Catchブロックには、例外が発生した場合の処理を書きます。 Finallyブロックには、ファイルのクローズや接続解放など、必ず実行したい後処理を書きます。

例外処理の例

Try
    Dim value As Integer = CInt(txtNumber.Text)
    MessageBox.Show("入力値:" & value)

Catch ex As FormatException
    MessageBox.Show("数値を入力してください。")

Catch ex As Exception
    MessageBox.Show("予期しないエラーが発生しました。")

End Try

この例では、数値変換に失敗した場合はFormatExceptionを捕捉し、 それ以外の例外はExceptionで捕捉しています。

ただし、すべての例外をExceptionでまとめて処理すると、 何が原因でエラーになったのか分かりにくくなります。 実務では、想定できる例外はできるだけ具体的な型で捕捉することが重要です。


2. Try / Catch / Finallyの実務的な使い方

Try / Catch / Finallyは便利ですが、使い方を誤ると、 エラー原因を隠してしまったり、不具合を見つけにくくしたりします。

悪い例:例外を握りつぶす

Try
    SaveData()
Catch ex As Exception
End Try

このコードは非常に危険です。 例外が発生しても何も表示されず、ログも残らないため、 データ保存に失敗していることに誰も気付けない可能性があります。

業務システムでは、例外を握りつぶす設計は避けるべきです。 少なくともログを出力し、必要に応じて利用者へメッセージを表示する必要があります。

良い例:ログを残して利用者に通知する

Try
    SaveData()

Catch ex As Exception
    Logger.Error("データ保存中にエラーが発生しました。", ex)
    MessageBox.Show("データ保存中にエラーが発生しました。管理者へ連絡してください。")

End Try

このように、詳細な例外情報はログに残し、 利用者には分かりやすいメッセージを表示するのが基本です。

Finallyでリソースを解放する

Dim connection As SqlClient.SqlConnection = Nothing

Try
    connection = New SqlClient.SqlConnection(connectionString)
    connection.Open()

    ' DB処理

Catch ex As Exception
    Logger.Error("DB処理でエラーが発生しました。", ex)
    Throw

Finally
    If connection IsNot Nothing Then
        connection.Dispose()
    End If

End Try

現在では、ConnectionやCommandなどのIDisposableなオブジェクトはUsingブロックで管理する方が一般的です。 ただし、Finallyの役割を理解しておくことは重要です。

Usingを使った推奨例

Try
    Using connection As New SqlClient.SqlConnection(connectionString)
        connection.Open()

        ' DB処理

    End Using

Catch ex As Exception
    Logger.Error("DB処理でエラーが発生しました。", ex)
    Throw

End Try

Usingを使うことで、例外が発生しても自動的にDisposeが呼び出されます。 DB接続、ファイル、Stream、Command、ReaderなどはUsingで管理するのが基本です。


3. 業務例外とシステム例外の分離

例外処理を設計するうえで重要なのが、業務例外とシステム例外を分けて考えることです。

業務例外とは

業務例外とは、システムとしては正常に動作しているが、 業務ルール上その処理を続行できない状態を表します。

  • 在庫数が不足している
  • 承認済みデータは削除できない
  • 入力された社員番号が既に存在している
  • 締め処理後のデータは更新できない
  • 権限がないため操作できない

これらはプログラムの不具合ではなく、業務上想定されるエラーです。 そのため、利用者に具体的な理由を表示する必要があります。

システム例外とは

システム例外とは、プログラムや実行環境で発生する想定外の異常です。

  • DB接続に失敗した
  • ファイルが存在しない
  • ネットワーク通信に失敗した
  • NullReferenceExceptionが発生した
  • SQL実行時に予期しないエラーが発生した

システム例外は、利用者がその場で解決できないことが多いため、 詳細情報はログに残し、画面には一般的なエラーメッセージを表示します。

業務例外クラスの例

Public Class BusinessException
    Inherits Exception

    Public Sub New(message As String)
        MyBase.New(message)
    End Sub

End Class

業務例外を発生させる例

Public Sub Approve(application As Application)

    If application.Status = "承認済み" Then
        Throw New BusinessException("既に承認済みの申請です。")
    End If

    application.Status = "承認済み"

End Sub

業務例外とシステム例外を分けて処理する例

Try
    _applicationService.Approve(applicationId)

    MessageBox.Show("承認しました。")

Catch ex As BusinessException
    MessageBox.Show(ex.Message)

Catch ex As Exception
    Logger.Error("承認処理中にシステムエラーが発生しました。", ex)
    MessageBox.Show("システムエラーが発生しました。管理者へ連絡してください。")

End Try

このように、業務例外は利用者に具体的に伝え、 システム例外はログに詳細を残して一般的なメッセージを表示する設計にします。


4. カスタム例外クラス

VB.NETでは、Exceptionクラスを継承して独自の例外クラスを作成できます。 カスタム例外を作ることで、エラーの種類を明確に分類できます。

基本的なカスタム例外

Public Class ValidationException
    Inherits Exception

    Public Property Errors As List(Of String)

    Public Sub New(errors As List(Of String))
        MyBase.New("入力チェックエラーが発生しました。")
        Me.Errors = errors
    End Sub

End Class

この例では、複数の入力エラーを保持できるValidationExceptionを定義しています。

入力チェックで利用する例

Public Sub Register(dto As UserRegisterDto)

    Dim errors As New List(Of String)()

    If String.IsNullOrWhiteSpace(dto.UserName) Then
        errors.Add("ユーザー名を入力してください。")
    End If

    If String.IsNullOrWhiteSpace(dto.Email) Then
        errors.Add("メールアドレスを入力してください。")
    End If

    If errors.Count > 0 Then
        Throw New ValidationException(errors)
    End If

    ' 登録処理

End Sub

画面側での処理例

Try
    _userService.Register(dto)

Catch ex As ValidationException
    MessageBox.Show(String.Join(Environment.NewLine, ex.Errors), "入力エラー")

Catch ex As Exception
    Logger.Error("ユーザー登録中にエラーが発生しました。", ex)
    MessageBox.Show("システムエラーが発生しました。")

End Try

例外クラスを分けることで、Catch側でエラーの種類に応じた処理をしやすくなります。

カスタム例外を使う場面

  • 業務ルール違反を表現したい場合
  • 入力チェックエラーをまとめて扱いたい場合
  • 外部APIエラーを独自に分類したい場合
  • 権限エラーや認証エラーを区別したい場合
  • 画面側でエラー種別ごとに表示を変えたい場合

ただし、何でもカスタム例外にすればよいわけではありません。 標準例外で十分に意味が伝わる場合は、標準例外を利用する方がシンプルです。


5. ログ設計の基本

ログは、システムの動作状況やエラー内容を記録するための重要な情報です。 障害発生時、ログが適切に残っていれば、原因調査や復旧対応がスムーズになります。

逆に、ログが不足していると、何が起きたのか分からず、 利用者への確認や再現調査に多くの時間がかかります。

ログに記録すべき情報

  • 発生日時
  • ログレベル
  • 処理名
  • 画面名または機能名
  • ユーザーID
  • 入力値や検索条件
  • エラーメッセージ
  • 例外の種類
  • StackTrace
  • SQL実行情報
  • 外部APIのレスポンス

ただし、パスワード、個人番号、クレジットカード番号などの機密情報をそのままログに出力してはいけません。 ログは調査に役立つ一方で、情報漏洩リスクにもなるため、出力内容を慎重に設計する必要があります。

ログレベル

ログレベル意味用途
Trace非常に詳細な処理追跡詳細デバッグや調査用
Debug開発・検証時の確認情報変数値や分岐確認
Info通常の処理記録ログイン、登録完了、バッチ開始など
Warn異常ではないが注意が必要リトライ成功、想定外入力、閾値超過など
Error処理失敗例外発生、DBエラー、外部連携失敗など
Fatalシステム継続困難な重大障害起動失敗、重要サービス停止など

ログ出力の例

Logger.Info("ユーザー登録処理を開始しました。UserName=" & dto.UserName)

Try
    _userService.Register(dto)
    Logger.Info("ユーザー登録処理が完了しました。UserName=" & dto.UserName)

Catch ex As Exception
    Logger.Error("ユーザー登録処理でエラーが発生しました。UserName=" & dto.UserName, ex)
    Throw

End Try

処理開始、処理終了、例外発生時にログを出すことで、 障害発生時にどこまで処理が進んだかを追跡しやすくなります。


6. ログ出力基盤の構築

ログ出力は、各画面や各クラスでバラバラに実装するのではなく、 共通のログ出力基盤を用意することが重要です。

VB.NETでは、簡易的なLoggerクラスを自作することもできます。 実務では、log4net、NLog、Serilogなどのログライブラリを使うこともあります。

簡易Loggerクラスの例

Public Class Logger

    Private Shared ReadOnly LogFilePath As String = "application.log"

    Public Shared Sub Info(message As String)
        WriteLog("INFO", message, Nothing)
    End Sub

    Public Shared Sub Warn(message As String)
        WriteLog("WARN", message, Nothing)
    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)

        Dim logMessage As New Text.StringBuilder()

        logMessage.AppendLine("[" & DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") & "] [" & level & "] " & message)

        If ex IsNot Nothing Then
            logMessage.AppendLine("ExceptionType: " & ex.GetType().FullName)
            logMessage.AppendLine("Message: " & ex.Message)
            logMessage.AppendLine("StackTrace: " & ex.StackTrace)
        End If

        IO.File.AppendAllText(LogFilePath, logMessage.ToString())

    End Sub

End Class

これは簡易的な例ですが、ログ出力を共通化することで、 出力形式や出力先を一元管理できます。

ログ設計で考慮すべきこと

  • ログファイルの保存場所
  • ログローテーション
  • ログレベルの切り替え
  • 本番環境と開発環境での出力差分
  • 個人情報や機密情報のマスキング
  • ログの保管期間
  • 障害調査時に必要な情報

特に本番環境では、ログファイルが肥大化し続けるとディスク容量を圧迫します。 日付ごとにログを分ける、一定サイズでローテーションする、古いログを削除するなどの運用設計が必要です。


7. StackTraceの読み方

StackTraceは、例外が発生するまでにどのメソッドがどの順番で呼び出されたかを示す情報です。 障害解析では非常に重要です。

StackTraceの例

System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。
   at SampleApp.UserService.Register(UserRegisterDto dto)
   at SampleApp.UserForm.btnRegister_Click(Object sender, EventArgs e)
   at System.Windows.Forms.Control.OnClick(EventArgs e)

この例では、UserFormのbtnRegister_ClickからUserService.Registerが呼ばれ、 その中でNullReferenceExceptionが発生したことが分かります。

StackTraceを見るポイント

  • 最初に自分たちのアプリケーションのクラス名を探す
  • 例外が発生したメソッドを確認する
  • 呼び出し元の流れを確認する
  • フレームワーク内部の情報に惑わされすぎない
  • InnerExceptionがある場合は必ず確認する

InnerExceptionの確認

Catch ex As Exception
    Logger.Error("処理中にエラーが発生しました。", ex)

    If ex.InnerException IsNot Nothing Then
        Logger.Error("内部例外が発生しました。", ex.InnerException)
    End If

End Try

例外が別の例外でラップされている場合、InnerExceptionに本当の原因が入っていることがあります。 DB連携、外部API、ファイル処理などでは特に重要です。


8. 障害発生時の調査観点

障害解析では、ただエラーメッセージを見るだけでは不十分です。 発生条件、再現性、入力値、データ状態、環境差分などを総合的に確認する必要があります。

障害調査で確認すること

  • いつ発生したか
  • 誰が操作したか
  • どの画面・機能で発生したか
  • どの操作をした直後に発生したか
  • 入力値や検索条件は何だったか
  • 対象データの状態はどうなっていたか
  • 同じ操作で再現するか
  • 特定の端末・ユーザー・権限でのみ発生するか
  • 直近でリリースや設定変更があったか
  • DB、ファイル、外部API、ネットワークに問題がないか

ログから確認すべき情報

  • 処理開始ログがあるか
  • 処理終了ログがあるか
  • どの処理で止まっているか
  • 例外の種類は何か
  • StackTraceはどこを指しているか
  • 同じ時間帯に他のエラーが出ていないか
  • 外部システム連携のレスポンスはどうだったか

障害調査の流れ

1. 利用者から発生状況を確認する
2. 発生日時と対象ユーザーを特定する
3. 該当時間帯のログを確認する
4. StackTraceから発生箇所を特定する
5. 入力値や対象データを確認する
6. 再現手順を作成する
7. 原因を分類する
8. 修正方針を決める
9. 修正後に再発防止策を検討する

障害解析では、「とりあえず直す」だけでなく、 なぜ発生したのか、なぜ検知できなかったのか、同じ問題が他にないかを確認することが重要です。


9. リトライ制御とフェイルセーフ設計

外部API通信、ネットワーク接続、ファイルアクセス、DB接続などでは、 一時的な失敗が発生することがあります。 このような一時的な失敗に対しては、リトライ制御が有効な場合があります。

リトライが有効な例

  • 一時的なネットワーク切断
  • 外部APIの一時的なタイムアウト
  • ファイルロックによる一時的な読み書き失敗
  • DBの一時的な接続失敗

リトライすべきでない例

  • 入力値が不正
  • 認証情報が間違っている
  • 権限がない
  • 業務ルールに違反している
  • SQL構文が間違っている

簡易リトライ処理の例

Public Sub ExecuteWithRetry(action As Action)

    Dim maxRetryCount As Integer = 3

    For retryCount As Integer = 1 To maxRetryCount

        Try
            action()
            Return

        Catch ex As TimeoutException

            Logger.Warn("タイムアウトが発生しました。リトライ回数:" & retryCount)

            If retryCount = maxRetryCount Then
                Throw
            End If

            Threading.Thread.Sleep(1000)

        End Try

    Next

End Sub

リトライ制御では、何でも再実行すればよいわけではありません。 同じ登録処理をリトライすると二重登録になる可能性があります。 そのため、処理の冪等性を考慮する必要があります。

フェイルセーフ設計

フェイルセーフとは、障害が発生した場合でも、被害を最小限に抑える設計です。

  • エラー時には処理を安全に停止する
  • 不完全なデータを登録しない
  • DB更新失敗時はRollbackする
  • 外部連携失敗時は再送可能な状態にする
  • 利用者に誤った成功メッセージを表示しない
  • 処理状態をログや管理テーブルに残す

業務システムでは、エラーが起きないことよりも、 エラーが起きたときに安全に止まれることが重要です。


10. 実務でありがちな例外処理・ログ設計の問題

Catchで何もしていない

例外を握りつぶすと、障害に気付けなくなります。 最低限、ログ出力を行う必要があります。

すべての例外を同じメッセージで表示している

入力ミスもDB障害もすべて「エラーが発生しました」と表示すると、 利用者は何をすればよいか分かりません。 業務エラーとシステムエラーは分けて表示する必要があります。

ログにStackTraceが出ていない

エラーメッセージだけでは、どこで例外が発生したのか分かりません。 例外ログにはStackTraceを含めるべきです。

個人情報をログに出している

調査のためとはいえ、パスワード、マイナンバー、クレジットカード番号などをログに出してはいけません。 必要な場合はマスキングします。

ログが多すぎて読めない

すべてをInfoログで出力すると、重要なエラーが埋もれてしまいます。 ログレベルを適切に使い分ける必要があります。

例外をThrow exで再スローしている

Catch ex As Exception
    Throw ex
End Try

この書き方は、StackTraceがリセットされる原因になります。 例外を再スローする場合は、以下のようにThrowだけを使います。

Catch ex As Exception
    Throw
End Try

StackTraceを維持するためにも、再スロー時はThrowを使うことが重要です。


11. 演習課題

演習1:業務例外クラスを作成する

BusinessExceptionクラスを作成し、在庫不足や承認済みデータの更新不可などを表現できるようにしてください。

演習2:入力チェック例外を実装する

ValidationExceptionクラスを作成し、複数の入力エラーをまとめて保持できるようにしてください。 画面側では、複数エラーを改行区切りで表示してください。

演習3:簡易Loggerクラスを作成する

Info、Warn、Errorの3種類のログをファイルへ出力するLoggerクラスを作成してください。 Errorログでは、例外メッセージとStackTraceも出力してください。

演習4:DB登録処理に例外処理を追加する

ユーザー登録処理にTry / Catchを追加し、 業務例外とシステム例外で表示メッセージを分けてください。

演習5:障害調査用ログを設計する

受注登録処理を想定し、処理開始、入力値、DB更新、処理完了、例外発生時にどのようなログを出すべきか設計してください。


12. まとめ

本章では、VB.NETにおける例外処理、ログ設計、障害解析について学習しました。

例外処理では、Try / Catch / Finallyの構文を理解するだけでなく、 どの例外をどこで捕捉し、どのように利用者へ通知し、どの情報をログに残すかを設計することが重要です。

業務システムでは、業務例外とシステム例外を分けて扱う必要があります。 業務例外は利用者が対応できる可能性があるため、具体的なメッセージを表示します。 一方、システム例外は利用者が解決できないことが多いため、詳細はログに残し、画面には一般的なメッセージを表示します。

ログ設計では、発生日時、ユーザーID、処理名、入力値、例外メッセージ、StackTraceなど、 障害解析に必要な情報を適切に記録します。 ただし、個人情報や機密情報をそのまま出力しないよう注意が必要です。

StackTraceを読めるようになると、例外がどのメソッドで発生し、 どのような呼び出し経路をたどったのかを把握できます。 これは障害解析において非常に重要なスキルです。

また、外部APIやネットワーク処理では、一時的な失敗に備えてリトライ制御を検討する必要があります。 ただし、二重登録や不整合が起きないよう、処理の冪等性やトランザクション設計にも注意が必要です。

本章のゴールは、エラーを単に隠すことではなく、正しく検知し、安全に停止し、原因を追跡できる設計を行うことです。 この考え方は、業務システムの本番運用、保守、障害対応において非常に重要な基礎となります。