[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("関数呼び出しが不正です。")

使用例のサンプル

サンプルの動作

  1. funcSample01プロシージャを呼び出す。
    funcSample01プロシージャの動作:
     Excelファイルのシートから、
     ファイルパスの文字列を配列として取得。
  2. funcSample02プロシージャを呼び出す。
    funcSample02プロシージャの動作:
     引数で指定されたパスのExcelファイルを開く。
     さらに、一枚目と二枚目のシートのA1セルに情報を書き込み、閉じる。

これらの関数内におけるエラーの発生と例外処理の流れを記したい。

使用環境

Windows 10 Home(64bit)
MSOffice 2016

Excelファイルとテスト用データについて

コードの記載されたExcelファイルのFilePathシートには
テスト用データのファイルの相対パスが記載されている。

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を呼び出すと、
システムエラーのメッセージと
ファイルパス取得失敗のメッセージが表示される。

しかし、例外処理を適切に組み込んでいるため、
処理が中断せずに次の行に続行することができる。

funcSample01エラー

ファイルパス取得失敗


funcSample02の呼び出しのループで
hoge.xlsxを起動してA1セルに書き込もうとすると、
すでに「あいうえお」と書き込まれているため、
カスタムエラーのメッセージと
イミディエイトウィンドウへのエラー情報出力が行われる。

funcSample02エラー

イミディエイトウィンドウへのエラー情報出力

しかし、こちらにおいても例外処理を適切に組み込んでいるため、
処理が中断せずに次の行に続行することができる。

サンプルとソースコードについて

こちらのリンクをご参照ください。


  1. 引用元:https://github.com/nkmk/python-snippets/blob/0bc3839319270c61ac37bd2112dd5996a4fe248b/notebook/exception_handling.py#L39-L51 ↩︎

関連記事

comments powered by Disqus

Translations: