VBAで安全にエラーハンドリングする

## VBAは「Try-Catch」ができない！？

「！？」なんて言ってますが、もちろんできないです。（笑）
ちなみに、VB.NETならできます。

ただ、Try-Catchがあろうなかろうが、**正しくエラーハンドリングができるかどうかは別**です。

大事なのは、モジュールを構造化したときに、

**正しい経路で、エラーを伝播できるか**

だと思います。
ということで、VBAにおけるエラーハンドリングについて説明していきます。

## 何もしなかったらどうなる？

こんな処理を考えてみましょう。

```vb
' こいつは上位
Sub Main()
    ErrorMethod
    
    MsgBox "完了しました。"
End Sub

' こいつは下位
Sub ErrorMethod()
    Err.Raise 513, "なにか起きた"
End Sub
```

このとき、Mainが上位、ErrorMethodが下位になります。
下位モジュールでエラーが起きてますね。

ちなみに、`Err.Raise`は実行時エラーを強制的に発生させるメソッドです。-> [Err.Raise](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/raise-method)

この場合、ErrorMethodの中で実行が止まります。

上位モジュールでエラーハンドリングをしていないからです。つまり

**何もしないと、エラーは上位に伝播する**

ということになります。
エラーハンドリングに使えるのは、下記のステートメントです。

1. `On Error Goto ラベル名`
2. `On Error Resume Next`

の２つがあります。
違いは、下記の2点です。

1. `On Error Goto`は、指定したラベルの位置まで処理が飛ぶ。
2. `On Error Resume Next`は、エラーを無視して突き進む。

では、エラーハンドリングの例を見ていきます。

## 上位モジュールでのエラーハンドリング

まずは、上位モジュールにおけるエラーハンドリングをしてみましょう。

下記の例は、`On Error Goto`の方式です。

```vb
Sub Main()
    On Error GoTo OnError

    ErrorMethod
    
    MsgBox "完了しました。"
    Exit Sub
OnError:
    MsgBox "なにか起きたらしい"
End Sub
```

`ErrorMethod`が呼び出された時点で、OnErrorラベルに処理が飛んでくれます。

`On Error Resume Next`を用いる場合はこうも書けます。

```vb
Sub Main()
    On Error Resume Next

    ErrorMethod
        
    If Err.Number = 0 Then
        MsgBox "完了しました。"
    Else
        MsgBox "なにか起きたらしい"
    End If
End Sub
```

このとき下記のように、後続処理を追加すると勝手に流れてしまう危険性があります。

```vb
Sub Main2()
    On Error Resume Next

    ErrorMethod

    ' 後続処理。勝手に流れて行ってしまう。
    OtherMethod
        
    If Err.Number = 0 Then
        MsgBox "完了しました。"
    Else
        MsgBox "なにか起きたらしい"
    End If
End Sub
```

なので、`On Error Goto`方式をお勧めします。
修正に強いコードを書くことは、そのまま安全なコードにつながるので、意識するといいでしょう。

## 下位モジュールでのエラーハンドリング

下位のモジュールで、エラーハンドリングの役割は、

**正しいエラーを上位に伝えること**

です。
実際に例を挙げていきます。

まず、最初にも書きましたが、何もしなければエラーは上位に伝播してくれます。
なので、必要がなければハンドリングを明示的にしなくて大丈夫です。
（Try-Catchも必要なければ書かないですよね）

```vb
Sub ErrorMethod()
    Err.Raise 513, "なにか起きた"
End Sub
```

ですが、ファイルIO等の処理を書いている際、
予期せずエラーが発生してしまうことがあります。

その際に、ファイルを閉じる処理（Close）を行わないと、ファイルが開きっぱなしになってしまいます。

そこで、下記のようにコードを直してあげる必要があります。

```vb
Sub ErrorMethodWithIO()
    On Error GoTo Dispose
   
    Open "path\to\file" For Input As #1

    Err.Raise 513, "ファイル読み込み中に、なにか起きた"
Dispose:
    ' エラーになったときも、必ず閉じる
    Close #1
End Sub
```

こうしてあげると、ファイルが閉じない心配はありません。

では、このモジュールを上位から呼び出すとどうなるでしょうか。

```vb
Sub Main()
    On Error GoTo OnError

    ErrorMethodWithIO
    
    MsgBox "完了しました。"
    Exit Sub
OnError:
    MsgBox "なにか起きたらしい" & vbCrLf & Err.Number
End Sub
```

実行すると分かりますが、「完了しました。」とでます。
つまり、**下位モジュールのエラーは握り潰されてしまった**ということです。

それではエラーが起きているのに、原因がわかりません。
（または、エラーが起きていることにすら気づけない。）

これを防ぐために、下位のモジュールでのエラーハンドリングの際は、必ずエラーを再度起こしてあげる必要があります。
その際つかえるのが、`Err.Raise`です。

```vb
Sub ErrorMethodWithIO()
    On Error GoTo Dispose
    
    Open "path\to\file" For Input As #1

    Err.Raise 513, "なにか起きた"
Dispose:
    Close #1
    
    ' エラーを再度起こしてあげる
    If Err.Number <> 0 Then
        Err.Raise Err.Number, Err.Source, Err.Description, Err.HelpFile, Err.HelpContext
    End If
End Sub
```

Try-Catchでいうところの、CatchでThrowしているイメージですね。

余談ですが、
よくThrowをしないでCatchでエラーをすり潰す例がありますが、危険ですよね。

> ただ、Try-Catchがあろうなかろうが、**正しくエラーハンドリングができるかどうかは別**です。

というのは、こういうことなのでした。

さて、もう１つ注意すべき点があります。
もう一度コードに修正を入れてみましょう。

```vb
Sub ErrorMethodWithIO()
    On Error GoTo Dispose
    
    ' ここ！！！！
    Err.Raise 514, "読み込む前に何かが起きた"
    
    Open "path\to\file" For Input As #1

    Err.Raise 513, "なにか起きた"

Dispose:
    Close #1
    
    ' エラーを再度起こしてあげる
    If Err.Number <> 0 Then
        Err.Raise Err.Number, Err.Source, Err.Description, Err.HelpFile, Err.HelpContext
    End If
End Sub
```

これは、**読み込みの処理の前**に、エラーが発生した場合です。

これが起きると、Disposeのラベルに飛んでしまい、開いてもいない#1をCloseしようとしてしまいます。
（例が悪いんですが、Closeは開いてもいない#1を閉じようとしても例外が発生しない模様。。。なので発生すると思って見て下さい）

こうなると、元のエラーが消え、別のエラー（ファイルクローズ失敗エラー）に変わってしまいます。
なので、少し工夫しましょう。

`On Error Goto`を読み込む直前に移動します。

```vb
Sub ErrorMethodWithIO()
    Err.Raise 514, "読み込む前に何かが起きた"
    
    ' 読み込む直前に移動させてあげる
    On Error GoTo Dispose
    
    Open "path\to\file" For Input As #1

    Err.Raise 513, "なにか起きた"
Dispose:
    Close #1
    
    ' エラーを再度起こしてあげる
    If Err.Number <> 0 Then
        Err.Raise Err.Number, Err.Source, Err.Description, Err.HelpFile, Err.HelpContext
    End If
End Sub
```

こうしてあげることで、本来のエラーである514番が上位に伝わり、原因の究明が容易になりますね。

## まとめ

大事な点は、下記になります。

- エラーがあった時点で後続処理が実行されないようにする -> **変更に強いコードにするため**
- エラーを握りつぶさず、最上位モジュールまで確実につなぐ -> **原因を確実に把握するため**

こうして、言語機能が貧弱と言われやすいVBAですが、
エラーハンドリングは、何とかなりそうなことがわかりました。

## 余談

私はプログラマーですが、正直VBAは嫌いです。
理由には、言語仕様や、公式ドキュメントの貧弱さ、パッケージングシステムの弱さ、Gitとの相性の悪さなど挙げればキリがないですが、
一番は、実装者のレベルの差が激しく、コードの負債が溜まりやすいことです。（それを回収？改修？していくことの辛さといったらもう…）

自前で作っているユーザーはあまりこういうことは考えないのかもしれません。
ただ、あまりにコアな業務にVBAを利用している場合、保守ができなくなり負債がたまって崩壊するときが来るかもしれません。

なので、そういう方もモジュールの構造化や、エラーハンドリングについて、この記事を機に考え直して貰えるなら、書いた甲斐があるというものです。
（簡単なものなら、ビジネスロジックとデータアクセスを分けるとかその程度でもいい。階層ごとに責務が分けられれば正直どんなでもいい。）

そして、そうした積み重ねが、後々の保守や開発効率に寄与するのは言うまでもないと思います。
自分、そして**次に直す人のため**、安全なエラーハンドリングを心がけていきましょう。