VBAのエラー内部構造を解説する

# はじめに
VBAに限らずですが、開発をしているときに必ずエラーをどうするかは頭を使う場所だと思います。
私がVBAで開発を行った際にもかなり頭を悩ませました。
特にVBAがエラーを内部でどう処理してどう管理しているのか、内部構造がよくわからず苦労しました。
ググってもググっても自分が納得するような説明が見当たらず、途方に暮れた覚えがあります。
しかし、開発をしていく中で仮説を立てながら実験し、かなり説明力のありそうな内部構造を見出すことができました。
過去の自分のため、同様の疑問を持っている方のためになればという思いで、稚拙ながらVBAのエラー処理の内部構造を解説してみたいと思います。

# 内部構造解説の構成
<dl>
    <dt>■VBAエラー処理のあれこれ</dt>
    <dd>最初に、エラーに関わるステートメントやプロパティ情報、VBAのエラー処理ルールを列挙していきます。</dd>
    <dt> ■サンプルコードで確認しよう ~プロシージャが1つの場合のエラーの流れ~</dt>
    <dd>そして、そのルールなどをサンプルコードを見ながら、プロシージャが1つの場合の基本的なエラーの流れを確認します。</dd>
    <dt>■サンプルコードで確認しよう ~プロシージャが複数の場合のエラーの流れ~</dt>
    <dd>さらに、プロシージャが複数の場合に関係してくるルールを説明をしていきます。</dd>
</dl>


### ■VBAエラー処理のあれこれ
<dl>
  <dt>1. プロシージャのエラープロパティ</dt>
  <dd>プロシージャは</dd>
  <dd>1. エラーステータス（エラー状態 or 非エラー状態）</dd>
  <dd>2. エラー発生行</dd>
  <dd>3. エラーラベル（エラー時の行先）</dd>
  <dd>を持つ</dd>
  <dd>※ 4. 呼び出しごとに初期化される。</dd>
  <dd><br></br></dd>
  <dt>2. プロシージャのエラー処理ルール<dt>
  <dd>プロシージャはエラー発生時、以下のルールに従ってエラーを対処する<dd>
  <dd>1. プロシージャのエラープロパティを設定する<dd>
  <dd>　　1. エラーステータスをエラー状態にする</dd>
  <dd>　　2. エラー発生行を記録する</dd>
  <dd>2. 実行行をエラーラベルに飛ばし処理を継続する</dd>
  <dd>3. ただし、対応できないエラー（※）が発生した場合、呼び出し元にエラーを投げる<dd>
  <dd>　　※ 1. エラーラベル未設定時のエラー</dd>
  <dd>　　※ 2. エラー状態でエラーが発生する</dd>
  <dd>4. 呼び出し元プロシージャはエラー発生プロシージャの呼び出し行でエラーが発生したとしてエラー処理を行う</dd>
  <dd>5. 対応できないエラーが発生し、呼び出し元プロシージャがない場合はデバッグ画面を表示する</dd>
  <dd><br></br></dd>
  <dt>3. On Errorステートメントの処理内容</dt>
  <dd>1. エラーラベル更新<dd>
  <dd>　　1. On Error Go To 0はエラーラベルの初期化に相当する</dd>
  <dd>　　2. エラー状態の場合はエラーラベルの更新はできない</dd>
  <dd>2. ErrオブジェクトのClearが行われる</dd>
  <dd>　　1. On Error Resume Nextでのエラー対処ではErrオブジェクトのClearは行われない</dd>
  <dd><br></br></dd>
  <dt>4. Resumeステートメントの処理内容</dt>
  <dd>1. ステートメントの移動（GoTo文相当）<dd>
  <dd>　　1.「Resume Next」の場合はエラー発生行の次の行へ移動する    
  <dd>2. ErrオブジェクトのClear</dd>
  <dd>3. エラーステータスの解除（エラー状態→非エラー状態）</dd>
  <dd>4. 非エラー状態での実行エラー発生が行われる</dd>
  <dd><br></br></dd>
  <dt>5. Errオブジェクトについて</dt>
  <dd>1. グローバル変数と同等
  <br>どこかのプロシージャでErrオブジェクトがClearされると、Errオブジェクトの内容は失われてしまう。</br></dd>
</dl>


### ■サンプルコードで確認しよう ~プロシージャが1つの場合のエラーの流れ~
(1)2度目のエラーでデバッグ画面が表示されるサンプルコード
```vb:mdl_Main.bas
Sub Sample_Error()
    Dim i As Long
    Dim sheetName As String
    On Error GoTo Nosheet  '<1>
    For i = 1 To 6
        sheetName = "No" & i
        ThisWorkbook.Sheets(sheetName).Activate
        Range("A1").Value = "このシートの名前は" & sheetName & "です" '<2>,<3>
Nosheet:
    Next i
End Sub
```

<1>On Error GoTo Nosheetステートメントを実行
　→エラーラベルを「NoSheet」に設定する（1-3）
　→errオブジェクトのClearをする（1-3）

<2>i=3（[No3]シート）でエラーが発生
→非エラー状態→エラー状態となる。（1-1）
→エラーラベルの通りNoSheetへ移動。（2-2）

<3>i=6（[No6]シート）でエラーが発生
→エラー状態でのエラーが発生。（1-1）
→対応できないエラー（エラー状態でエラーが発生）となる。（2-3-2）
→呼び出し元プロシージャがないため、デバッグ画面が表示される（2-4）

##### (2) 2度目のエラーでもデバッグ画面を表示させない例1
```vb:mdl_Main.bas
Sub Sample_NotError1()
    Dim i As Long
    Dim sheetName As String
    
    On Error GoTo Nosheet  '<1>
    
    For i = 1 To 6
        sheetName = "No" & i
        ThisWorkbook.Sheets(sheetName).Activate
        Range("A1").Value = "このシートの名前は" & sheetName & "です"  '<2>,<3>
continue:
    Next i
    
    Exit Sub
Nosheet:
    Resume continue
End Sub
```
<1>On Error GoTo Nosheetステートメントを実行
　→エラーラベルを「NoSheet」に設定する。（1-3）
　→errオブジェクトのClearをする。（3-2）

<2>i=3（[No3]シート）でエラーが発生
→非エラー状態→エラー状態となる。（2-1-1）
→エラーラベルの通りNoSheetへ移動。（2-2）
→Resume continueステートメントを実行
　errオブジェクトがClearされる。（4-2）
　エラー状態→非エラー状態となる。（4-3）
→continueラベルから処理を継続（4-1）

<3>i=6（[No6]シート）でエラーが発生
→エラーラベルの通りNoSheetへ移動。（2-2）
　非エラー状態のため、「対応できないエラー」とならず処理が継続
→i=3のエラー処理と同じ処理が行われる。

##### (3) 2度目のエラーでもデバッグ画面を表示させない例2
```vb:mdl_Main.bas
Sub Sample_NotError2()
    Dim i As Long
    Dim sheetName As String
    
    On Error Resume Next  '<1>
    
    For i = 1 To 6
        sheetName = "No" & i
        ThisWorkbook.Sheets(sheetName).Activate  '<2>,<3>
        Range("A1").Value = "このシートの名前は" & sheetName & "です"  
    Next i
    
    On Error GoTo 0  '<4>
End Sub
```
<1>On Error Resume Nextステートメントを実行
　→エラーラベルを「NoSheet」に設定する（1-3）
　→errオブジェクトのClearをする（3-2）

<2>i=3（[No3]シート）でエラーが発生
→非エラー状態→エラー状態となる。（2-1-1）
→エラーラベルが「Resume Next」のため、
　エラー発生行の次の行へ移動する。（4-1）
　ErrオブジェクトのClear（4-2）
　エラーステータスの解除（エラー状態→非エラー状態）（4-3）
　が実行され、処理継続。

<3>i=6（[No6]シート）でエラーが発生
→非エラー状態→エラー状態となる。（2-1-1）
→エラーラベルが「Resume Next」のため、
　エラー発生行の次の行へ移動する。（4-1）
　ErrオブジェクトのClear（4-2）
　エラーステータスの解除（エラー状態→非エラー状態）（4-3）
　が実行され、処理継続。

<4>On Error GoTo 0ステートメントを実行
　→Go To 0のため、
　エラーラベルの初期化（3-1-1）
　ErrオブジェクトのClear（3-2）
　が実行される。

 <追記>
このサンプルプログラムでは不要であるが、On Error GoTo 0を書くことで

* On Error Resume Next～On Error GoTo 0でエラーが握りつぶされる範囲が明示される
* Errオブジェクトをクリアしておくことで握りつぶしたエラーの情報をきれいにしておける

のでOn Error GoTo 0ステートメントを付け加えるのがおすすめ。

### ■サンプルコードで確認しよう ~プロシージャが複数の場合のエラーの流れ~
##### (4) Sample_Errorでプロシージャを分割したパターン
```vb:mdl_Main.bas
Sub Sample_Main_ErrorAfterError1()
    Dim i As Long
    Dim sheetName As String
    Dim errMsgs(5) As String
    
    On Error GoTo Nosheet  '<1>
    
    For i = 1 To 6
        sheetName = "No" & i
        Call mdl_Other.WriteSheetName(sheetName, "A1")  '<2>,<3>
continue:
    Next i
    
    ThisWorkbook.Sheets("エラー").Range("A1") = errMsgs
    Exit Sub
    
Nosheet:
'    エラーの記録
    errMsgs(i) = sheetName & "の処理でエラーが発生しました。" & vbCr & Err.Description

    Resume continue
End Sub
```
```vb:mdl_Other.bas
Sub WriteSheetName( _
    ByVal target_sheet_name As String, ByVal target_range As String _
)
    ThisWorkbook.Sheets(target_sheet_name).Activate
    Range(target_range).Value = "このシートの名前は" & target_sheet_name & "です"  
End Sub
```
<1>On Error GoTo Nosheetステートメントを実行
　→エラーラベルを「NoSheet」に設定する。（1-3）
　→errオブジェクトのClearをする。（3-2）

<2>i=3（[No3]シート）でエラーが発生
→非エラー状態→エラー状態となる。（2-1）
→対応できないエラー（※）が発生したため、エラーを呼び出し元に投げる。（2-3）
　※エラーラベル未設定時のエラー（2-3-1）
→Call WriteSheetName(sheetName, "A1")ステートメントのエラーとして処理継続（Sample_Main_Error）（2-4）
→エラーラベルの通りNoSheetへ移動。（2-2）

<3>i=6（[No6]シート）でエラーが発生
→非エラー状態→エラー状態となる。（2-1）
　呼び出しごとでプロシージャのエラープロパティは初期化され、非エラー状態になっていることに注意
→対応できないエラー（※）が発生したため、エラーを呼び出し元に投げる（2-3）
　※エラーラベル未設定時のエラー（2-3-1）
→Call WriteSheetName(sheetName, "A1")ステートメントのエラー（2-4）
　エラー状態でのエラーとなる。
→対応できないエラーとなる。（2-3）
　※エラー状態でエラーが発生。（2-3-2）
→呼び出し元プロシージャがないため、デバッグ画面が表示される。（2-5）

##### (5) エラー発生後の処理でエラーが発生してしまう例1
```vb:mdl_Main.bas
Sub Sample_Main_ErrorAfterError1()
    Dim i As Long
    Dim sheetName As String
    Dim errMsgs(5) As String
    
    On Error GoTo Nosheet
    
    For i = 1 To 6
        sheetName = "No" & i
        Call mdl_Other.WriteSheetName(sheetName, "A1")  '<1>
continue:
    Next i
    
    ThisWorkbook.Sheets("エラー").Range("A1") = errMsgs
    Exit Sub
    
Nosheet:
'    エラーの記録
    errMsgs(i) = sheetName & "の処理でエラーが発生しました。" & vbCr & Err.Description  '<2>

    Resume continue
End Sub
```
<1>i=6（[No6]シート）の時、Call WriteSheetName(sheetName, "A1")でエラー発生
　→エラーラベルの通りNoSheetへ移動。（2-2）

<2>エラーの記録処理でエラーが発生
　※errMsgs配列が0から5までのため、「インデックスが有効範囲にありません。」エラーが出る。
→対応できないエラー（エラー状態でエラーが発生）となる。（2-3）
→呼び出し元プロシージャがないため、デバッグ画面が表示される。（2-5）

##### (6) エラー発生後の処理でエラーが発生してしまう例2
```vb:mdl_Main.bas
Sub Sample_Main_ErrorAfterError2()
    Dim i As Long
    Dim sheetName As String
    Dim errMsgs(5) As String
    
    On Error GoTo Nosheet
    
    For i = 1 To 6
        sheetName = "No" & i
        Call mdl_Other.WriteSheetName(sheetName, "A1")  '<1>
continue:
    Next i
    
    ThisWorkbook.Sheets("エラー").Range("A1") = errMsgs
    Exit Sub
    
Nosheet:
    On Error Resume Next  '<2>
'    エラーの記録
    errMsgs(i) = sheetName & "の処理でエラーが発生しました。" & vbCr & Err.Description

    Resume continue
End Sub
```
<1>i=6（[No6]シート）の時、Call WriteSheetName(sheetName, "A1")でエラー発生
　→エラーラベルの通りNoSheetへ移動。（2-2）

<2>On Error Resume Nextステートメントを実行
　しかし、エラー状態の場合はエラーラベル更新をできない（3-1-2）
　→Resume Nextがエラーラベルにセットされない
　→サンプルコード(5)と同様、デバッグ画面が表示されてしまう

##### (7) エラー発生後の処理でエラーが発生しないようにする例
```vb:mdl_Main.bas
Sub Sample_Main_NotErrorAfterError()
    Dim i As Long
    Dim sheetName As String
    Dim errMsgs(5) As String
    Dim errMsg As String
    Dim errDescription As String
    
    On Error GoTo Nosheet
    
    For i = 1 To 6
        sheetName = "No" & i
        Call mdl_Other.WriteSheetName(sheetName, "A1")  '<1>,<3>
continue:
    Next i
    
    ThisWorkbook.Sheets("エラー").Range("A1") = errMsgs
    Exit Sub
    
Nosheet:
    errDescription = Err.Description
'    エラーの記録
    Call AppendErrorMsg(errMsgs, i, errMsg)  '<2>,<4>

    Resume continue
End Sub
```
```vb:mdl_Other.bas
Sub WriteSheetName( _
    ByVal target_sheet_name As String, ByVal target_range As String _
)
    ThisWorkbook.Sheets(target_sheet_name).Activate
    Range(target_range).Value = "このシートの名前は" & target_sheet_name & "です"
End Sub

Sub AppendErrorMsg( _
    ByRef err_msgs As Variant, ByVal arr_num As Long, ByVal err_msg As String _
)
    On Error Resume Next
    err_msgs(arr_num) = err_msg
    On Error GoTo 0
End Sub
```
<1>i=3（[No6]シート）の時、Call WriteSheetName(sheetName, "A1")でエラー発生
　→エラーラベルの通りNoSheetへ移動。（2-2）

<2>エラーの記録処理実行（AppendErrorMsg）
　→On Error Resume Nextステートメントを実行
　→i=3ではエラー発生しないためエラーなく処理が継続する

<3>i=6（[No6]シート）の時、Call WriteSheetName(sheetName, "A1")でエラー発生
　→エラーラベルの通りNoSheetへ移動。（2-2）

<4>エラーの記録処理実行（AppendErrorMsg）
　→On Error Resume Nextステートメントを実行
　→i=6ではエラー発生するが、エラーラベルが「Resume Next」であるため、エラーなく処理が継続する

【ポイント】
エラー記録処理を別プロシージャにくくりだし、そこでOn Error Resume Nextでエラーを握りつぶす。
そうすればSample_Main_NotErrorAfterErrorのエラー処理内ではエラーが発生していないことになる。
【注意】
AppendErrorMsgプロシージャでOn Error Resume Nextステートメントを実行しているため、ErrオブジェクトがClearされることに注意。
それを見越して、エラー処理内でerrDescription = Err.Descriptionとしてエラー内容を保持している。

# さいごに
おおよそ一通り解説してみました。
本質的な内容としては「■VBAエラー処理のあれこれ」の記載がすべてな気がします。
わかりづらい説明になってしまいましたが、参考になれば幸いです。