[VBA] 例外処理の典型的なパターン&使用例サンプル
概要
この記事について
プログラミングにおいて、想定内のエラーが発生した場合に
(あるいは自作のエラーを定義し、そのエラーが発生した際に)
例外として専用の処理を行うという実装は一般的なものである。
VBAにも例外処理の機能は備わっているが、
後発の言語よりも使用方法がややこしい部分があるため、
この記事で典型的な例外処理のパターンのテンプレートと、
使用例としてのサンプルを記したい。
説明のために作成したExcelファイルとソースコード、テスト用データはこちらでダウンロードできます。
例外処理とは
例外処理の仕組み
例外処理が記述されていないプログラムを実行する場合について考える。
そのコードの処理を実行する際にエラーが発生すると、
コンソールやファイルに出力したり、エラーメッセージが表示されるなど、
環境によって既定の動作が起こり、
そのエラーが発生した行において処理が中断したりする。
ツールやアプリケーションの場合、通常それはあまり好ましくない動作となる。
なぜなら、エラーが起きたら今までの処理を復旧したり、ユーザにわからないようにリカバリーの処理を動かしたい場合があるため。
そのため、例外処理という特別な処理の記述があると非常に便利になる。
すなわち、正常時およびエラーが起きた場合で処理を分岐させ、
それぞれの特別な処理というものを追加する。
例外処理の典型例(Pythonのコードによる)
try-except
例えばPythonだとこのように記述する1。
1def divide_each(a, b):
2 try:
3 print(a / b)
4 except ZeroDivisionError as e:
5 print('catch ZeroDivisionError:', e)
6 except TypeError as e:
7 print('catch TypeError:', e)
8
9divide_each(1, 0) #一回目の呼び出し
10# catch ZeroDivisionError: division by zero
11
12divide_each('a', 'b') #二回目の呼び出し
13# catch TypeError: unsupported operand type(s) for /: 'str' and 'str'
divide_each関数は
2つの引数a / b
の除算を計算し、
その際に起こり得るエラーを捕捉(キャッチ)して
それぞれ特有の処理を実行する。
捕捉される実行時エラーは、
- 分母がゼロだった場合のエラー(ZeroDivisionError)
- 引数に数字以外の型の値を指定し、除算ができない場合のエラー(TypeError)
であり、
前者の場合はコンソール(標準出力)に「catch ZeroDivisionError: division by zero」と出力され、
後者の場合は「catch TypeError: unsupported operand type(s) for ...」と出力される。
このようなPythonの構文を「 try-except 」と呼ぶ。
finallyによる終了時処理
正常に処理が進んだ場合も、エラーが発生してexcept句でキャッチされた場合も、
共通で終了時に必ず実行される処理を行いたい場合は、
次のように「
finally句
」を追加する。
1def divide_each(a, b):
2 try:
3 print(a / b)
4 except ZeroDivisionError as e:
5 print('catch ZeroDivisionError:', e)
6 except TypeError as e:
7 print('catch TypeError:', e)
8 finally:
9 print('passed end processing')
10
11
12divide_each(1, 0)
13# catch ZeroDivisionError: division by zero
14
15divide_each('a', 'b')
16# catch TypeError: unsupported operand type(s) for /: 'str' and 'str'
17
これを実行すると、
次のようにコンソールに出力される。
1catch ZeroDivisionError: division by zero
2passed end processing
3catch TypeError: unsupported operand type(s) for /: 'str' and 'str'
4passed end processing
VBAにおける例外処理
特徴
VBAの場合、
煩雑なことに、Pythonの場合の
try-except構文
のように
定形構文の形で例外処理が定義されていない。
これはつまり、エラーが起きたらコード上のどの行にジャンプし、
その後終了処理としてどの行にジャンプするのかということを
自力で定義しなければならないということである。
そのために用いるステートメントとして、
GotoステートメントおよびOn Errorステートメントというものが存在する。
Gotoステートメント | プロシージャ内で ラベルを貼られた行に 無条件で移動する |
Official Link |
On Errorステートメント | エラー発生時のプログラムの挙動を定義する。 Gotoと組み合わせてエラー発生時に指定の行に移動したり、 エラー時の分岐処理自体を無効化させたりする。 |
Official Link |
テンプレート
VBAには、
SubプロシージャとFunctionプロシージャという、
大きく二種類のプロシージャが存在する。
それぞれに対して、
典型的な例外処理のテンプレートを記したい。
Subプロシージャ
1'******************************************************************************************
2'*関数名 :Subプロシージャの例外処理テンプレート
3'*機能 :
4'*引数 :
5'******************************************************************************************
6Public Sub subTemplate()
7
8 '定数
9 Const FUNC_NAME As String = "subTemplate"
10
11 '変数
12
13 On Error GoTo ErrorHandler
14
15 '---ここから処理を記載する---
16
17
18ExitHandler:
19
20 '---ここから終了処理を記載する---
21
22 Exit Sub
23
24ErrorHandler:
25
26 '---ここから例外発生時処理を記載する---
27 ' 例:メッセージボックス表示、
28 ' ログファイルにシステムエラー情報書き込み、
29 ' システムエラー発生の通知メールの作成・発信など
30
31 MsgBox "システムエラーが発生しました。" & _
32 vbLf & _
33 "関数名:" & FUNC_NAME & _
34 vbLf & _
35 "エラー番号:" & Err.Number & vbNewLine & _
36 Err.Description, vbCritical, "マクロ"
37
38 GoTo ExitHandler
39
40End Sub
On Error GoTo ErrorHandler
の宣言によって、
エラー発生時にErrorHandler行ラベルに移動することが設定される。
ErrorHandlerは上記のPythonコードにおけるexcept句に相当する。
ログファイルへの書き込みやメッセージ表示などのエラー処理の終了後、
GoTo ExitHandlerの命令によって
終了処理に移動する。
これは、上記のPythonコードにおけるfinally句に相当する。
Functionプロシージャ
Functionプロシージャの場合、
呼び出し元の関数にエラーが発生したことを伝達する方法によって
二種類のテンプレートが考えられる。
テンプレート①
1'******************************************************************************************
2'*関数名 :Functionプロシージャの例外処理テンプレート(1)
3'*機能 :
4'*引数 :
5'*戻り値 :True > 正常終了、False > 異常終了
6'******************************************************************************************
7Public Function functionTemplate01() As Boolean
8
9 '定数
10 Const FUNC_NAME As String = "functionTemplate01"
11
12 '変数
13
14 On Error GoTo ErrorHandler
15
16 functionTemplate01 = False
17
18 '---ここから処理を記載する---
19
20TruePoint:
21
22 '---ここから正常時のみの終了処理を記載する---
23
24 functionTemplate01 = True
25
26ExitHandler:
27
28 '---ここから終了処理を記載する---
29
30 Exit Function
31
32ErrorHandler:
33
34 '---ここから例外発生時処理を記載する---
35 ' 例:メッセージボックス表示、
36 ' ログファイルにシステムエラー情報書き込み、
37 ' システムエラー発生の通知メールの作成・発信など
38
39 MsgBox "システムエラーが発生しました。" & _
40 vbLf & _
41 "関数名:" & FUNC_NAME & _
42 vbLf & _
43 "エラー番号:" & Err.Number & vbNewLine & _
44 Err.Description, vbCritical, "マクロ"
45
46 GoTo ExitHandler
47
48End Function
Subプロシージャのテンプレートとの違いは、
Boolean型の関数の戻り値が存在し、Trueならば正常終了、Falseならば異常終了となることである。
エラー発生時ではfunctionTemplate01 = True
を通過しないため
呼び出し元の関数に、異常終了という形でエラーが伝達される。
1if not functionTemplate01() then Call Msgbox("関数呼び出しが不正です。")
また、TruePoint行ラベルが追加され、
処理の途中で正常終了としてプロシージャを抜けたい場合に
Exit Sub
の代わりにGoto TruePoint
の命令を記述し、
正常終了としてこのプロシージャを抜ける。
テンプレート②
1'******************************************************************************************
2'*関数名 :Functionプロシージャの例外処理テンプレート(2)
3'*機能 :
4'*引数 :
5'*戻り値 :任意の指定の基本型 > 正常終了、Null > 異常終了
6'******************************************************************************************
7Public Function functionTemplate02() As Variant
8
9 '定数
10 Const FUNC_NAME As String = "functionTemplate02"
11
12 '変数
13
14 On Error GoTo ErrorHandler
15
16 functionTemplate02 = Null
17
18 '---ここから処理を記載する---
19
20ExitHandler:
21
22 '---ここから終了処理を記載する---
23
24 Exit Function
25
26ErrorHandler:
27
28 '---ここから例外発生時処理を記載する---
29 ' 例:メッセージボックス表示、
30 ' ログファイルにシステムエラー情報書き込み、
31 ' システムエラー発生の通知メールの作成・発信など
32
33 MsgBox "システムエラーが発生しました。" & _
34 vbLf & _
35 "関数名:" & FUNC_NAME & _
36 vbLf & _
37 "エラー番号:" & Err.Number & vbNewLine & _
38 Err.Description, vbCritical, "マクロ"
39
40 GoTo ExitHandler
41
42End Function
Subプロシージャのテンプレートとの違いは、
Variant型の関数の戻り値が存在し、Null以外の任意の型の戻り値ならば正常終了、Nullが返れば異常終了と判定できることである。
処理の最初にfunctionTemplate02 = Null
としてNUllを代入し、
処理の途中で関数として戻したい値を代入する。
特に戻り値が必要なければ空欄でも代入しておけば良い。
呼び出し元の関数で、異常終了を検知する場合は
isNull
関数を用いる。
1Dim returnValue as Variant
2returnValue = functionTemplate02()
3if isNull(returnValue) then Call Msgbox("関数呼び出しが不正です。")
使用例のサンプル
サンプルの動作
- funcSample01プロシージャを呼び出す。
funcSample01プロシージャの動作:
Excelファイルのシートから、
ファイルパスの文字列を配列として取得。 - funcSample02プロシージャを呼び出す。
funcSample02プロシージャの動作:
引数で指定されたパスのExcelファイルを開く。
さらに、一枚目と二枚目のシートのA1セルに情報を書き込み、閉じる。
これらの関数内におけるエラーの発生と例外処理の流れを記したい。
使用環境
Windows 10 Home(64bit)
MSOffice 2016
Excelファイルとテスト用データについて
コードの記載されたExcelファイルのFilePathシートには
テスト用データのファイルの相対パスが記載されている。
テスト用データfoo、barのExcelファイルは
一枚目のシートのA1セルが空欄であり、
hogeファイルは一枚目のシートのA1セルに「あいうえお」と記入されている。
bar.xlsxは二枚目のシートが存在する。
処理フロー図
コード
呼び出し元のSubプロシージャ
1'******************************************************************************************
2'*関数名 :例外処理Subプロシージャ実例
3'*機能 :
4'*引数 :
5'******************************************************************************************
6Public Sub subSample()
7
8 '定数
9 Const FUNC_NAME As String = "subSample"
10
11 '変数
12 Dim filePathArr As Variant
13 Dim filePath As Variant
14 Dim sheetName As String
15
16 On Error GoTo ErrorHandler
17
18 Application.ScreenUpdating = False
19
20 'funcSample01の呼び出し 存在しないシート名の引数で呼び出す
21 sheetName = "sheetNotExist"
22 filePathArr = funcSample01(sheetName)
23 '戻り値がNullであるためメッセージ表示
24 If IsNull(filePathArr) Then MsgBox sheetName & "シートは存在しません。" & vbNewLine & "ファイルパス配列の取得に失敗しました(処理は続行します)。"
25
26 'funcSample01の呼び出し 存在するシート名の引数で呼び出す
27 sheetName = "FilePath"
28 filePathArr = funcSample01(sheetName)
29 '戻り値がNullではないため失敗の表示なし
30 If IsNull(filePathArr) Then MsgBox sheetName & "シートは存在しません。" & vbNewLine & "ファイルパス配列の取得に失敗しました(処理は続行します)。"
31
32 'それぞれのExcelファイルについて、funcSample02を呼び出す
33 For Each filePath In filePathArr
34 'funcSample02の呼び出し
35 'すでにA1セルが書き込まれていた場合は、イミディエイトウィンドウに失敗したファイルパスを出力
36 If Not funcSample02(ThisWorkbook.Path & filePath) Then
37 Debug.Print "書き込み失敗ファイル:" & filePath
38 End If
39 Next filePath
40
41
42 '■■■funcSample01,funcSample02などでキャッチできなかった想定外のエラーは
43 ' このプロシージャのErrorHandler行ラベルでキャッチされます。
44
45ExitHandler:
46
47 Application.ScreenUpdating = True
48
49 Exit Sub
50
51ErrorHandler:
52
53 MsgBox "システムエラーが発生しました。" & _
54 vbLf & _
55 "関数名:" & FUNC_NAME & _
56 vbLf & _
57 "エラー番号:" & Err.Number & vbNewLine & _
58 Err.Description, vbCritical, "マクロ"
59
60 GoTo ExitHandler
61
62End Sub
funcSample01プロシージャ
1'******************************************************************************************
2'*関数名 :例外処理Functionプロシージャ実例(1)
3'*機能 :ファイルパスの文字列を配列として取得
4'*引数 :このファイルのシートの名前
5'*戻り値 :文字列の配列 > 正常終了、Null > 異常終了
6'******************************************************************************************
7Public Function funcSample01(ByVal wsName As String) As Variant
8
9 '定数
10 Const FUNC_NAME As String = "funcSample01"
11
12 '変数
13
14 On Error GoTo ErrorHandler
15
16 funcSample01 = Null
17
18 '指定されたシートのA1セルからA3セルまでの値を配列として取得する
19 With ThisWorkbook.Worksheets(wsName)
20 funcSample01 = .Range("A1:A3").Value
21 End With
22
23ExitHandler:
24
25
26 Exit Function
27
28ErrorHandler:
29
30 MsgBox "システムエラーが発生しました。" & _
31 vbLf & _
32 "関数名:" & FUNC_NAME & _
33 vbLf & _
34 "エラー番号:" & Err.Number & vbNewLine & _
35 Err.Description, vbCritical, "マクロ"
36
37 GoTo ExitHandler
38
39End Function
テンプレートの項でも記載したように、
正常終了ならばfuncSample01の戻り値はファイルパス文字列配列であり、
異常終了ならばNullである。
戻り値がNullの場合、
呼び出し元のsubSampleプロシージャにおいて、
「ファイルパス配列の取得に失敗しました(処理は続行します)」のメッセージが表示される。
funcSample02プロシージャ
1'******************************************************************************************
2'*関数名 :例外処理Functionプロシージャ実例(2)
3'*機能 :指定されたパスのエクセルファイルを開く
4' 一枚目のシートのA1セルに時刻を書き込む
5' 二枚目のシートが存在すれば、二枚目のA1セルに「完了」と書き込む
6'*引数 :エクセルファイルのパス
7'*戻り値 :True > 正常終了、False > 異常終了
8'******************************************************************************************
9Public Function funcSample02(ByVal filePath As String) As Boolean
10
11 '定数
12 Const FUNC_NAME As String = "funcSample02"
13
14 '変数
15 Dim wb As Workbook
16
17 On Error GoTo ErrorHandler
18
19 funcSample02 = False
20
21 Set wb = Workbooks.Open(filePath)
22
23
24 With wb
25 '一枚目のシートのA1セルに時刻を書き込む
26 'すでにA1セルに文字が書き込まれていた場合はエラーとなる(異常終了)
27 If Trim(.Worksheets(1).Range("A1").Value) <> "" Then Err.Raise 1000, , "A1セルにすでに値が存在します。"
28 .Worksheets(1).Range("A1").Value = Now
29
30 '二枚目のシートが存在しなければ終了(正常終了)
31 If .Worksheets.Count < 2 Then GoTo TruePoint
32
33 '二枚目のA1セルに「完了」と書き込む
34 .Worksheets(2).Range("A1").Value = "完了"
35
36 End With
37
38
39TruePoint:
40
41 'シートの保存
42 wb.Save
43
44 funcSample02 = True
45
46ExitHandler:
47
48 '正常終了時でもエラーが起きた場合でも、必ずブックを閉じる
49 If Not wb Is Nothing Then wb.Close SaveChanges:=False
50
51 Exit Function
52
53ErrorHandler:
54
55 MsgBox "システムエラーが発生しました。" & _
56 vbLf & _
57 "関数名:" & FUNC_NAME & _
58 vbLf & _
59 "エラー番号:" & Err.Number & vbNewLine & _
60 Err.Description, vbCritical, "マクロ"
61
62 GoTo ExitHandler
63
64End Function
65
テンプレートの項でも記載したように、
正常終了ならばfuncSample01の戻り値はTrueであり、
異常終了ならばFalseである。
ここで、
例外処理の強み
の一つである、
「
エラー発生時に今までの動作の復旧を図る
」処理が記されている。
1ExitHandler:
2 '正常終了時でもエラーが起きた場合でも、必ずブックを閉じる
3 If Not wb Is Nothing Then wb.Close SaveChanges:=False
この部分で、
正常終了時でもエラー発生時の挙動においても
必ずブックを閉じるようにしているため、
ブックが閉じられないまま残ってしまうという事態を
避けることができる。
また、二枚目の処理が行われるのは、
テスト用データのブックにシートが二枚以上存在する場合のみであり、
もし一枚しかシートが存在しなければ、そのまま正常終了として終了処理に移動する。
1'二枚目のシートが存在しなければ終了(正常終了)
2If .Worksheets.Count < 2 Then GoTo TruePoint
また、戻り値がFalseの場合、
呼び出し元のsubSampleプロシージャにおいて、
イミディエイトウィンドウ(ログファイルの代わり)に失敗したファイルパスが出力される。
1If Not funcSample02(ThisWorkbook.Path & filePath) Then
2 Debug.Print "書き込み失敗ファイル:" & filePath
3End If
実際に実行すると
subSampleを実行する。
①
sheetNotExistを引数としてfuncSample01を呼び出すと、
システムエラーのメッセージと
ファイルパス取得失敗のメッセージが表示される。
しかし、例外処理を適切に組み込んでいるため、
処理が中断せずに次の行に続行することができる。
②
funcSample02の呼び出しのループで
hoge.xlsxを起動してA1セルに書き込もうとすると、
すでに「あいうえお」と書き込まれているため、
カスタムエラーのメッセージと
イミディエイトウィンドウへのエラー情報出力が行われる。
しかし、こちらにおいても例外処理を適切に組み込んでいるため、
処理が中断せずに次の行に続行することができる。
サンプルとソースコードについて
こちらのリンクをご参照ください。
関連記事
- [VBA] クラスを利用するメリットと方法について & 簡単なサンプル(1)
- [Excel VBA] 個人的に作業がはかどった自作Excelショートカット
- [Excel VBA]ポリモーフィズムを用いて、IF文を使わずラジオボタンごとの処理分岐を行う
- [Excel VBA]選択フォルダ配下のエクセルブックの全シートでA1にカーソル移動させるツールを作成した
- [Excel VBA] 見積書作成ツールを作成した