例外処理のベストプラクティス(VB/コンソールプログラム編)

VB

何故この記事を投稿しようと思ったのか

普段業務の中でVBをメインで使用しています。他人が作成したプログラムの改修を行うこともあるのですが、例外処理について思うところがあり、記事にまとめました。

例外エラーの重要性について

作成したプログラムには必ず不具合が潜んでいると思っています。なので、例外が発生した時に「どこで例外エラーが発生したのか」「どのような例外エラーが発生したのか」を正確に把握することは極めて重要な課題であると考えています。

重要な課題にも関わらず、この課題に対する意識が薄いエンジニアが非常に多い印象です。特に自分が一切関わっていないシステムで発生した障害(例外エラー)の調査を任されたときに痛い目にあいます。

私がこれまでに経験した障害調査の中で、思わず泣きたくなった悪しき慣習(バッドプラクティス)をご紹介します。ぜひご覧になっている皆様の中に、このバッドプラクティスに陥っていないか確認してみてください。

思わず泣きたくなったバッドプラクティス

握りつぶす

論外ですね。例外エラーを握りつぶしてそのまま処理を続行させるケースです。そもそも例外エラーは、”例外”であって、想定されていないエラーが発生していることを意味していますので、処理を続行して良いわけがありません。

VB
Module Module1

    Sub Main()
        Debug.WriteLine("処理開始")
        Method1()
        Debug.WriteLine("処理終了")
    End Sub

    Public Sub Method1()
        Try
            Dim a As Integer
            a = "A" 'Integer型の変数に文字を代入するので例外が発生する
        Catch ex As Exception
            'ここで何もしていないので、例外エラーは握りつぶされる
        End Try
    End Sub

End Module
Plaintext
処理開始
処理終了

もし、既知の例外エラーが発生し、その場合のみ処理を継続させたいときはまだ許せますが、最低限ログ出力は行ってほしいところです。

VB
Module Module1

    Sub Main()
        Debug.WriteLine("処理開始")
        Method1()
        Debug.WriteLine("処理終了")
    End Sub

    Public Sub Method1()
        Try
            Dim a As Integer
            a = "A" 'Integer型の変数に文字を代入するので例外が発生する
        Catch icex As InvalidCastException
            '型変換のエラーは想定されるため、ログを出力した上で処理を継続させる
            Debug.WriteLine("既知の型変換エラーが発生しました。処理は継続します。例外エラーの詳細は以下を確認してください。")
            Debug.WriteLine(icex.ToString())
        End Try
    End Sub

End Module
Plaintext
処理開始
既知の型変換エラーが発生しました。処理は継続します。例外エラーの詳細は以下を確認してください。
System.InvalidCastException: String "A" から型 'Integer' への変換は無効です。 ---> System.FormatException: 入力文字列の形式が正しくありません。
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ParseDouble(String Value, NumberFormatInfo NumberFormat)
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
   --- 内部例外スタック トレースの終わり ---
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
   場所 ConsoleTesting.Module1.Method1() 場所 C:\ConsoleTesting\Module1.vb:行 12
処理終了

内容を出力しない

せっかく例外エラーを捕捉しているのに、そのエラー内容を出力せず、独自のエラーメッセージを出力しているケースです。これでは例外エラーの内容がわからないので、結局デバッグ実行して例外エラーを確認する必要が出てきます。

VB
Module Module1

    Sub Main()
        Debug.WriteLine("処理開始")
        Method1()
        Debug.WriteLine("処理終了")
    End Sub

    Public Sub Method1()
        Try
            Dim a As Integer
            a = "A" 'Integer型の変数に文字を代入するので例外が発生する
        Catch ex As Exception
            'エラーは捕捉しているものの、独自のメッセージを出力しているだけなので、
            '例外エラーの内容がわからない。
            Debug.WriteLine("例外エラーが発生しました。")
            Throw
        End Try
    End Sub

End Module
Plaintext
処理開始
例外エラーが発生しました。

再スローする

例外エラーを再スロー(Throw ex)しているケースです。こうすると、本来発生した箇所の例外エラーの情報が失われ、再スローしている部分で例外が発生したように見えてしまいます。上記と同じになりますが、結局はデバッグ実行で根本の例外エラーを突き止めなければなりません。

VB
Module Module1

    Sub Main()
        Try
            Debug.WriteLine("処理開始")
            Method2()
            Debug.WriteLine("処理終了")
        Catch ex As Exception
            Debug.WriteLine("例外エラーが発生しました。(Main)")
            'ex.ToStringで出力されるスタックトレースには、
            'Method2で発生させた独自例外の内容しか出力されていない。
            Debug.WriteLine(ex.ToString)
        End Try
    End Sub

    Public Sub Method2()
        Try
            Method1()
        Catch ex As Exception
            '新たな独自例外を発生させているので、
            'Method1で発生した例外エラーの内容がわからなくなる
            Throw New Exception("例外エラーが発生しました。(Method2)")
        End Try
    End Sub

    Public Sub Method1()
        Dim a As Integer
        a = "A" 'Integer型の変数に文字を代入するので例外が発生する
    End Sub

End Module
Plaintext
処理開始
例外エラーが発生しました。(Main)
System.Exception: 例外エラーが発生しました。(Method2)
   場所 ConsoleTesting.Module1.Method2() 場所 C:\ConsoleTesting\Module1.vb:行 22
   場所 ConsoleTesting.Module1.Main() 場所 C:\ConsoleTesting\Module1.vb:行 6

思わず泣きたくなったバッドプラクティスは以上です。次に、個人的なベストプラクティスをご紹介します。

個人的なベストプラクティス

最上位1か所で捕捉する

例外エラーに対する考え方に統一性がないと、Try~Catch構文が不用意にあちこちに記述されていきます。そうすると、握りつぶされたりする可能性が高まるため、基本的には最上位1か所で捕捉し、異常終了させる方針が良いと思います。こうすることで、ソースコードの可読性が向上します。

VB
Module Module1

    Sub Main()
        Try
            Debug.WriteLine("処理開始")
            Method2()
            Debug.WriteLine("処理終了")
        Catch ex As Exception
            Debug.WriteLine("例外エラーが発生しました。(Main)")
            'ex.ToStringで出力されるスタックトレースには、
            'Method1で発生した例外情報が出力される
            Debug.WriteLine(ex.ToString)
            Debug.WriteLine("異常終了")
        End Try
    End Sub

    Public Sub Method2()
        'Method2では例外を捕捉しない
        Method1()
    End Sub

    Public Sub Method1()
        Dim a As Integer
        a = "A" 'Integer型の変数に文字を代入するので例外が発生する
    End Sub

End Module
Plaintext
処理開始
例外エラーが発生しました。(Main)
System.InvalidCastException: String "A" から型 'Integer' への変換は無効です。 ---> System.FormatException: 入力文字列の形式が正しくありません。
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ParseDouble(String Value, NumberFormatInfo NumberFormat)
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
   --- 内部例外スタック トレースの終わり ---
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
   場所 ConsoleTesting.Module1.Method1() 場所 C:\ConsoleTesting\Module1.vb:行 24
   場所 ConsoleTesting.Module1.Method2() 場所 C:\ConsoleTesting\Module1.vb:行 19
   場所 ConsoleTesting.Module1.Main() 場所 C:\ConsoleTesting\Module1.vb:行 6
異常終了

内容を細かく出力する

例外情報を出力する際、「ex.Message」と記載されているソースコードが多いですが情報量が少ないので、「ex.ToString」とします。こうすることでスタックトレースの情報も出力されるようになります。

「ex.Message」と「ex.ToString」の違いは以下の通りです。

VB
Module Module1

    Sub Main()
        Try
            Debug.WriteLine("処理開始")
            Method2()
            Debug.WriteLine("処理終了")
        Catch ex As Exception
            Debug.WriteLine("例外エラーが発生しました。(Main)")
            'ex.ToStringで出力されるスタックトレースには、
            'Method1で発生した例外情報が出力される
            Debug.WriteLine("■ex.Messageの場合 " & vbCrLf & ex.Message)
            Debug.WriteLine("■ex.ToStringの場合 " & vbCrLf & ex.ToString)
            Debug.WriteLine("異常終了")
        End Try
    End Sub

    Public Sub Method2()
        'Method2では例外を捕捉しない
        Method1()
    End Sub

    Public Sub Method1()
        Dim a As Integer
        a = "A" 'Integer型の変数に文字を代入するので例外が発生する
    End Sub

End Module
Plaintext
処理開始
例外エラーが発生しました。(Main)
■ex.Messageの場合 
String "A" から型 'Integer' への変換は無効です。
■ex.ToStringの場合
System.InvalidCastException: String "A" から型 'Integer' への変換は無効です。 ---> System.FormatException: 入力文字列の形式が正しくありません。
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ParseDouble(String Value, NumberFormatInfo NumberFormat)
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
   --- 内部例外スタック トレースの終わり ---
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
   場所 ConsoleTesting.Module1.Method1() 場所 C:\ConsoleTesting\Module1.vb:行 25
   場所 ConsoleTesting.Module1.Method2() 場所 C:\ConsoleTesting\Module1.vb:行 20
   場所 ConsoleTesting.Module1.Main() 場所 C:\ConsoleTesting\Module1.vb:行 6
異常終了

スローする

呼び出したメソッド内で例外エラーが発生した時、捕捉はしたくないけど、独自の処理を行いたいような場合は、以下のような記述となります。こうすることで元々の例外情報を残しつつ、独自の処理も行うことができます。

VB
Module Module1

    Sub Main()
        Try
            Debug.WriteLine("処理開始")
            Method2()
            Debug.WriteLine("処理終了")
        Catch ex As Exception
            Debug.WriteLine("例外エラーが発生しました。(Main)")
            'ex.ToStringで出力されるスタックトレースには、
            'Method1で発生した例外情報が出力される
            Debug.WriteLine(ex.ToString)
            Debug.WriteLine("異常終了")
        End Try
    End Sub

    Public Sub Method2()
        Try
            Method1()
        Catch ex As Exception
            'ここでMethod1で例外が発生したときに行う処理を記載します。
            '例えば、ファイルの解放やデータベースのクローズ処理が該当するかと思います。
            'file.Close()
            'database.Close()
            '処理が終わったら最後に「Throw」します。
            'くれぐれも「Throw ex」としないように気を付けてください。
            Throw
        End Try
    End Sub

    Public Sub Method1()
        Dim a As Integer
        a = "A" 'Integer型の変数に文字を代入するので例外が発生する
    End Sub

End Module
Plaintext
処理開始
例外エラーが発生しました。(Main)
System.InvalidCastException: String "A" から型 'Integer' への変換は無効です。 ---> System.FormatException: 入力文字列の形式が正しくありません。
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ParseDouble(String Value, NumberFormatInfo NumberFormat)
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
   --- 内部例外スタック トレースの終わり ---
   場所 Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
   場所 ConsoleTesting.Module1.Method1() 場所 C:\ConsoleTesting\Module1.vb:行 33
   場所 ConsoleTesting.Module1.Method2() 場所 C:\ConsoleTesting\Module1.vb:行 27
   場所 ConsoleTesting.Module1.Main() 場所 C:\ConsoleTesting\Module1.vb:行 6
異常終了

ご紹介した3つのベストプラクティスを意識することで、例外エラーと上手く付き合えるようになると思います。皆さんもぜひ参考にしてみてください。

コメント

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