Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第22回 例外処理の決定

2020/10/4

この記事が対象とする製品・バージョン

VB2019 Visual Basic 2019 対象です。
VB2017 Visual Basic 2017 対象です。
VB2015 Visual Basic 2015 対象です。
VB2013 Visual Basic 2013 対象です。
VB2012 Visual Basic 2012 対象です。
VB2010 Visual Basic 2010 対象です。
VB2008 Visual Basic 2008 対象です。
VB2005 Visual Basic 2005 対象です。
VB.NET 2003 Visual Basic.NET 2003 対象外ですがほとんどの説明があてはまるので参考になります。
VB.NET 2002 Visual Basic.NET (2002) 対象外ですがほとんどの説明があてはまるので参考になります。
VB6対応 Visual Basic 6.0 × 対象外です。

 

目次

1.例外処理の決定

例外が発生した場合に何が実行されるかは、おおよそ次の図に用に決定します。

例外処理の決定

この図は少し簡略化して書いており、この図の通りでない場合も若干ありますが、とてもレアなケースですの割愛することにします。

例外が発生したときに最終的に実行される処理は次のどれかです。

Try ~ End Try の中に 発生した例外を捕まえられる Catch がない場合は、どの例外処理も実行されずに通常のフローに復帰します。通常のフローに復帰する場合は End Tryの次の行から実行されます。Retrunなど、Catchの中から直接呼出し元に戻ることもできます。

Catchについて前回説明しましが、例外発生時に実行される処理は Catch だけではありません。今回は残りの2つである「未処理の例外処理」と「フレームワークの既定の例外処理」について説明します。これら3つの例外処理を使って効率の良いプログラムを書くようにしましょう。結論を先に書くと私は、ほとんどの例外処理は「未処理の例外処理」に記述するのがよく、Catch を使用すべき場合は、かなりレアであると思っています。

メモ メモ  -  「未処理の例外処理」という名前

この機能には特別な名前は付いていないので、この記事では「未処理の例外処理」と呼ぶことにします。

 

メモ メモ  -  古い構文 On Error

この他に、On Error というエラー処理の構文があります。Try ~ Catch の構文はVB.NET(2002)からは 導入されたので、2002年以降のプログラムでは Try ~ Catch を使うのが普通です。ほぼ20年前のことなので On Error を見かけることは現在ではまずないでしょう。

 

 

2.フレームワークの既定の例外処理

Catchでの例外処理も後述する 未処理の例外処理も何も記述されていない場合は、フレームワークの既定の例外処理が実行されます。

既定の例外処理がどのようなものであるかはアプリケーションの種類によって異なります。

たとえば、Windowsフォームアプリケーションの場合、次のようなメッセージが表示されます。

例外が発生してプログラムが終了する

 

ASP.NET Core Webアプリケーションの場合、これはインターネットのようなネットワークに公開して、ブラウザーで使用するタイプのアプリケーションなので、ブラウザーにエラー画面が表示されます。

ブラウザー上にエラーが表示される

メモ メモ  -  Webアプリケーションはエラー画面の情報で攻撃されます

この画像のように詳細な例外情報を第三者に表示することは、攻撃する材料を与えてしまうことになるので、本番運用しているシステムではもっと情報をしぼって表示することになります。

 

Web系のプロジェクトの場合、Windowsのイベントログにも例外の情報が記録されます。

イベントログに記録された例外

 

メモ メモ  -  イベントログの確認方法

コントロールパネルで 「システムとセキュリティ」→「管理ツール」→「イベントログの表示」でイベントビューアー起動。イベントビューアーで、Windowsログのアプリケーションの一覧の中に例外情報のログが生成されます。

 

このほかにもアプリケーションの種類によってフレームワークの既定の例外処理は異なります。

 

3.未処理の例外処理

3-1.概要

どこでも Catch されなかった例外の処理を記述する場所をフレームワークが提供してくれている場合があります。

たとえば、Windowsフォームアプリケーションの場合、UnhandledExceptionイベント(読み方:UnhandledException=アンハンドルドエクセプション)がそれです。

アプリケーションの特性によって最適な例外処理が変わるため、他の種類のアプリケーションでは、また別の仕組みが用意されています。いくつかピックアップしてみます。

これらの使い方は、それぞれ異なるため、ここでは代表でWindowsフォームアプリケーション と コンソールアプリケーションの場合を説明します。

他の種類のアプリケーションの場合、未処理の例外処理の利用方法は異なりますが、考え方としてはこのようなものがなにかしら存在すると思いますので、アプリケーションの種類に応じて未処理の例外処理方法を調査して使いこなすようにしてください。

※調査するのが面倒で、もう 至るところに Try ~ Catch を付けてしまう ということもあるようですが、これは後で苦労することが多いので、事前に調査して最適な例外処理をすることをお奨めします。

 

3-2.Windowsフォームアプリケーション

Windowsフォームアプリケーションでは、どこにもCatchされていない例外が発生した場合、アプリケーションの UnhandledExceptionイベント が発生します。これはイベントなので、このイベントを捕まえて処理を記述することで 未処理の例外処理を実行できます。このイベントは特別扱いされており、プログラムの方法が特殊です。イベントハンドラーを記述する方法を説明します。

 

まず、ソリューションエクスプローラーでプロジェクトを右クリックし、プロパティを開きます。

プロパティ画面ではアプリケーションタブから「アプリケーション イベント表示」をクリックします。

※このボタンは、真ん中辺りにある「アプリケーション フレームワークを有効にする」にチェックが入っていないとクリックできません。

アプリケーションイベントの表示

 

ApplicationEvents.vbというファイルが開くので、画面上部中央のドロップダウンリストから MyApplicationイベントを選択し、次に右側から UnhandledException を選択します。

ApplicationEvents.vb

そうすると、次のように UnhandledExceptionイベントのイベントハンドラーが生成されます。

ここに「例外発生!」というメッセージを表示するだけのプログラムを記述してみましょう。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Imports Microsoft.VisualBasic.ApplicationServices

Namespace My
    ' 次のイベントは MyApplication に対して利用できます:
    ' Startup:アプリケーションが開始されたとき、スタートアップ フォームが作成される前に発生します。
    ' Shutdown:アプリケーション フォームがすべて閉じられた後に発生します。このイベントは、アプリケーションが異常終了したときには発生しません。
    ' UnhandledException:ハンドルされない例外がアプリケーションで発生したときに発生します。
    ' StartupNextInstance:単一インスタンス アプリケーションが起動され、それが既にアクティブであるときに発生します。
    ' NetworkAvailabilityChanged:ネットワーク接続が接続されたとき、または切断されたときに発生します。
    Partial Friend Class MyApplication
        Private Sub MyApplication_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs) Handles Me.UnhandledException
            MsgBox("例外発生!")
        End Sub
    End Class
End Namespace

 

そして、実際にこの未処理の例外処理を動かしてみましょう。次のようにわざと例外が発生するプログラムを作ります。このプログラムは Nothing の プロパティを呼び出しているので、NullReferenceException が発生します。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim s As String '初期化していないので s は Nothing
        Dim x As Integer = s.Length
    End Sub
End Class

Visual Studioで普通に実行すると、例外が発生した時点で停止して例外ヘルパー(別名 例外処理アシスタント)が表示されてしまうので、未処理の例外処理を試すには、[デバッグ]メニューの [デバッグなしで開始]をクリックして実行します。

デバッグなしで開始

これを使って実行すると、「例外発生!」と表示されることが確認できます。

 

UnhandledExceptionイベント内では引数 e の Exceptionプロパティを使って発生した例外情報を取得することもできます。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Imports Microsoft.VisualBasic.ApplicationServices

Namespace My
    ' 次のイベントは MyApplication に対して利用できます:
    ' Startup:アプリケーションが開始されたとき、スタートアップ フォームが作成される前に発生します。
    ' Shutdown:アプリケーション フォームがすべて閉じられた後に発生します。このイベントは、アプリケーションが異常終了したときには発生しません。
    ' UnhandledException:ハンドルされない例外がアプリケーションで発生したときに発生します。
    ' StartupNextInstance:単一インスタンス アプリケーションが起動され、それが既にアクティブであるときに発生します。
    ' NetworkAvailabilityChanged:ネットワーク接続が接続されたとき、または切断されたときに発生します。
    Partial Friend Class MyApplication
        Private Sub MyApplication_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs) Handles Me.UnhandledException
            Dim originalException As Exception = e.Exception

            MsgBox(originalException.GetType.Name) 'たとえば、NullReferenceException と表示されます。
            MsgBox(originalException.Message) '原因となった例外のもつメッセージを表示します。例:オブジェクト参照がオブジェクト インスタンスに設定されていません。
        End Sub
    End Class
End Namespace

 

また、e.ExitApplicationプロパティに False を設定することで、アプリケーションが終了しないようにすることもできます。

これは既定の例外処理で表示される下記ダイアログで「続行」をクリックしたときの動作と似ています。

例外が発生してプログラムが終了する

この、未処理の例外が発生しているのにアプリケーションを終了させない という動作は、何かよくわからない異常が起こっているけど、処理を続行する という意味になり非常に危険です。通常は使用すべきではないでしょう。

初級プログラマーで、エラー発生時にプログラムが終了しないことを非常に重視する方をときおり見かけますが、通常最も重視すべきはユーザーのデータの保護です。つまり、データが壊れたり、消えたりする可能性があるのだったら、そんな危険な状態のプログラムは終了してしまったほうがましということです。

 

3-3.コンソールアプリケーション

コンソールアプリケーションでもWindowsフォームアプリケーションでも未処理の例外が発生すると AppDomainクラス(読み方:AppDomain=アップドメイン)の UnhandledExceptionイベントが発生するので、これを捕まえることで未処理の例外に対する例外処理を記述できます。

AppDomainクラスは、.NETのアプリケーションを実行環境を表すクラスで、.NETのアプリケーションを実行する場合には必ず存在しています。このクラスのインスタンスにアクセスするには AppDomain.CurrentDomainプロパティを使用します。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Imports System

Module Program
    Sub Main(args As String())

        '未処理の例外が発生した場合、 AppDomain_UnhandledException を実行するようにする。
        AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf AppDomain_UnhandledException

        Dim s As String '初期化していないので s は Nothing
        Dim x As Integer = s.Length

        Console.WriteLine("Hello World!")
    End Sub

    Private Sub AppDomain_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs)

        Console.WriteLine("例外発生!")

    End Sub
End Module

このサンプルでは初級講座ではまだ説明していないことがいくつか登場します。まず、そもそもコンソールアプリケーションの作成方法や考え方を説明していません。それから、AddHandler(アドハンドラー)を使ってイベントを捕まえる方法も説明していません。

ここで私が知ってほしいことはこういった事柄ではなく、未処理の例外処理の方法は1つではないということです。前述したようにアプリケーションの種類によって利用できる仕組みが違うのです。Windowsフォームアプリケーションを1つ紹介しただけではこの違いを示せないので、まだ説明していないことがいくつかでてくるにもかかわらずコンソールアプリケーションの場合について紹介したわけです。

未処理の例外をイベントとして処理する点と、そのイベントの名前が UnhandledException である点は似ています。他はあまり似ていません。

たとえば、コンソールアプリケーションの例で紹介している AppDomainのの UnhandledException内では元の例外情報を取得するには  e.ExceptionObjectプロパティを使用します。このプロパティは Object型なので例外として使用しやすくするためには Exception クラスへの型変換が必要です。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

 Private Sub AppDomain_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs)

    Dim originalException As Exception = DirectCast(e.ExceptionObject, Exception)

    Console.WriteLine(originalException.GetType.Name) 'たとえば、NullReferenceException と表示されます。
    Console.WriteLine(originalException.Message) '原因となった例外のもつメッセージを表示します。

End Sub

それから、Windowsフォームアプリケーションで使用できた e.ExitApplication というプロパティは存在しませんし、その代わりになる機能もありません。

Webアプリケーションになると未処理の例外処理の仕組みはこれらとは大きく異なります。ただ、それを説明するにはWebアプリケーションのことをある程度知っている必要があるのでここでは紹介しないことにします。

 

4.呼び出し履歴(スタックトレース)

4-1.呼び出し履歴の確認

前回も触れましたが、VBを含むほとんどのプログラミング言語では メソッドA から メソッドB を呼び出し、メソッドB から メソッドC を呼び出し、・・・という呼び出しの履歴を記録します。この記録をスタックトレースと言います。メソッドCの実行が終わったら、メソッドBに戻り、その後はメソッドAに戻るというようにどこに戻るか記憶しておくために使うのが主目的です。それに加えて複雑なプログラムを作っているときに何が起こっているのか整理するのにも役に立ちます。

少しVisual Studioで実験してみましょう。

Windowsフォームアプリケーションでボタンを1つ配置し、次のようにプログラムしてみます。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        MethodA()
    End Sub

    Private Sub MethodA()
        MethodB()
    End Sub

    Private Sub MethodB()
        MethodC()
    End Sub

    Private Sub MethodC()
        '何もしない
    End Sub
End Class

そして、Private Sub MethodC の行でブレークポイントを設定します。

ブレークポイントは第10回 条件判断 If でも登場しています。プログラムの実行を一時停止するポイントのことです。実行がこの行に到達すると一時停止するので、ゆっくり呼び出し履歴を確認することができます。

ブレークポイントの設定

この状態でプログラムを実行し、ボタンをクリックすると、MethodCが呼び出された瞬間に実行が停止して、次のような画面になります。

MethodCで実行停止

この状態のとき、[デバッグ]メニューから[ウィンドウ] - [呼び出し履歴] をクリックすると呼び出し履歴ウィンドウが開いて、呼び出し履歴が表示されます。

呼び出し履歴

予想通り、Button1_Click → MethodA → MethodB → MethodC と呼び出されたことがわかります。

ところで、Button1_Click はどこから呼び出されたのでしょうか? 1つ下の行に [外部コード] と書いてあります。

[外部コード] というのは、あなたのプログラムではないということです。右クリックして、「外部コードの表示」をクリックすると詳細が見られますので確認してみましょう。

外部コードを含む呼び出し履歴

そうすると、Button1_Click以前にも実はたくさんの履歴があることがわかります。これら外部コードはアプリケーションの動作に必要な処理をうまくやってくれて、しかもあなたが意識しなくて良いように普段は隠されています。これらの外部コードのおかげでWindowsフォームアプリケーションを作るプログラマーはボタンをぺたぺた貼り付けてClickイベントなどにプログラムを書くだけ、つまり、興味のあることしかやらないで済むのです。

あなたが配置したボタンを表示し、マウスがいつクリックされていもいいように監視し、クリックされたらボタンの外見を変化させ、Clickイベントを発生させる・・・というような退屈なコードは外部コードが面倒を見てくれています。

 

4-2.呼び出し履歴と Try ~ Catch の関係

Try ~ Catch による例外処理は、呼び出し先で発生した例外も捕まえてくれます。

たとえば、MethodCで例外が発生するようにわざとNothingからメソッドを呼んでみます。Nothingのメソッドを呼び出すとNullReferenceExceptionが発生します。そして、MethodA にのみ Try ~ Catch を記述してみます。結果は MethodC で発生した例外を MethodA がキャッチします。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        MethodA()
    End Sub

    Private Sub MethodA()
        Try
            MethodB()
        Catch ex As Exception
            MsgBox("MethodAで捕まえました。")
        End Try
    End Sub

    Private Sub MethodB()
        MethodC()
    End Sub

    Private Sub MethodC()
        Dim s As String '初期化していないので s は Nothing
        Dim x As Integer = s.Length
    End Sub
End Class

さきほどのブレークポイントは用済みなのでもう1回クリックして解除して置いてください。

実行すると、予想通り「MethodAで捕まえました。」と表示されます。

 

今度は MethodC にも Try ~ Catch を書いてみましょう。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        MethodA()
    End Sub

    Private Sub MethodA()
        Try
            MethodB()
        Catch ex As Exception
            MsgBox("MethodAで捕まえました。")
        End Try
    End Sub

    Private Sub MethodB()
        MethodC()
    End Sub

    Private Sub MethodC()
        Try
            Dim s As String '初期化していないので s は Nothing
            Dim x As Integer = s.Length
        Catch ex As Exception
            MsgBox("MethodCで捕まえました。")
        End Try
    End Sub
End Class

実行すると、今度は「MethodCで捕まえました。」と表示されます。「MethodAで捕まえました。」は表示されません。

 

このように例外が発生した場合、VBはスタックトレースを遡(さかのぼ)っていき、最初に該当する Catch に記述された例外処理を実行します。

このことから、同じ例外に対する例外処理でも、どこに Try ~ Catch を書けばよいのか選択肢があることがわかります。

 

4-3.呼び出し履歴と 未処理の例外処理 の関係

[外部コード]にもTry Catchが組み込まれている場合があります。

さきほどのスタックトレースの中に DebuggableCallback というメソッドがあり、このメソッドの中には Try があります。

外部コードに存在するTry

この Try には Catch がないので特別な例外処理は行われないのですが、デバッグなしで実行 や Visual Studio 外で exe を直接実行などしている場合は、このメソッドは 「CallBack」という名前のメソッドに置き換わります。Callbackメソッドの場合、Try に加えて Catch が記述されており、どこでもCatchされなかった例外がここまで遡ってくるとここで Catch してしまいます。

メモ メモ  -  メソッドが置き換わる?

実行環境や構成によって異なるクラスやメソッドを呼び出すようにプログラムすることが可能です。マイクロソフトはこの仕掛けをとても上手に使っており、普段は気にしなくても良いようになっています。

今回も説明のためにあえて裏の仕組みを説明していますが、本来は不要な知識です。また、マイクロソフトはこの仕組みを保証しているわけではないのでメソッドの名前や細かい仕様はアナウンスなく突然変更される可能性もあります。

ここで例外がCatchされると、UnhandledExceptionイベント(読み方:UnhandledException=アンハンドルドエクセプション)が発生します。このイベントハンドラーを記述していない場合、前回も紹介したようなハンドルされていない例外のダイアログを表示します。

例外が発生してプログラムが終了する

 

UnhandledExceptionイベントハンドラーを記述している場合、そのプログラムが実行されます。

 

発展 発展学習  -  中身を見てみる

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

DebuggableCallbackとCallbackのソースコードのリンクを貼っておきます。VBではなく、C#で作られているのですが、try ~ catch は同じ書き方なのでなんとなく雰囲気はわかります。

どちらも try が付いていて、DebuggableCallbackの方には catch がなく、Callbackの方には catch があります。

DebuggableCallbackのソースコード

https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/NativeWindow.cs,ad40308c5b6490dd

Callbackのソースコード

https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/NativeWindow.cs,ff91744537fa674a