上級者を目指すExcelVBA #1 『エラー処理』

# はじめに

私はExcelをよく使う職場にいて、Webアプリケーションでやるような複雑なプログラムをVBAで何度も作ってきました。
このシリーズでは、今までVBAで蓄積してきたノウハウを少しずつ書いていきたいと思います。


## このシリーズの対象者
* Excelファイルを会社等でよく使う
* VBAの基本的な文法は理解している
* VBAの中級者、上級者を目指したい


# VBAのエラー処理について

本編に入ります。
ExcelVBAでまず最初に意識していただきたいのはエラー処理の方法です。

VBAでエラー処理を書かずにエラーが発生した場合、以下のような「実行時エラー」のウィンドウが出て処理が止まります。

<img width="432" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200881/cbbe421e-fcb1-f405-71db-f0cdb24179a4.png">


以下ではエラー処理を全く書かずにわざとエラーを発生させています。

```vba:FileReader.bas
' サンプル用コード
’ エラー処理を書かずに存在しないシートを選択
'
Public Sub sample()
    ' 存在しないシートを選択
     ThisWorkbook.Worksheets("存在しないシート").Activate
End Sub
```

この実行時エラーが出ると以下のデメリットがあります。

:::note alert
<strong>デメリット</strong>
- 無視して良い軽微なエラーでも処理が止まる
- ユーザと開発者が異なる場合、エラー発生箇所とエラー内容が開発者に伝わりにくい
　　（※ユーザが画面をキャプチャして伝える必要あり）
- エラーメッセージが抽象的でユーザには分かりにくい 
:::

これは絶対にNGです。
まずはこの実行時エラーのウィンドウを表示させないことを考えましょう。
　
## 対策 1： すべての関数にエラー処理をつける

コードは複数の関数に細かく分割し、すべての関数の先頭に「On Error GoTo〜」を付け、関数末尾にエラー処理を書きましょう。

「On Error GoTo〜」以下のコードでエラーが発生した場合、実行時エラーウィンドウが表示されなくなり、処理も途中で止まりません。

```vba:FileReader.bas
'　サンプル用コード
'　関数の先頭、末尾にエラー処理を付与
'
Public Sub sample()
    On Error GoTo ErrHdl
    
    ' 存在しないシートを選択
    ThisWorkbook.Worksheets("存在しないシート").Activate
ErrHdl:
    '  エラー発生時のみ、以下コードが実行される
    If Err.Number <> 0 Then
        Debug.Print "FileReader.sample", Err.Description
    End If
End Sub
```

If Err.Number <> 0 then〜以下のコードは、エラーが発生した場合のみ実行されます。

そのため、エラー処理はこのIf文内部に記述します。
上記の例ではエラーが発生した場所とエラー内容をイミディエイトウィンドウにデバッグ出力しています。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200881/be2c05cb-2601-4f54-30ae-b88bee84a594.png">

上記の"FileReader.sample"の箇所は、エラー発生箇所のモジュール名と関数名を出力するために書いています。
ただ、モジュール名を毎回エラー処理内部に書くのは面倒なので、モジュールの先頭では定数でモジュール名を定義しておくと便利です。

```vba:FileReader.bas
Option Explicit
 
'======================================================
'
' サンプル用モジュール
'
'======================================================

' モジュール名 ※モジュール先頭に定数で定義
Const MODULE_NAME = "FileReader"

```

この場合、エラー処理内部のモジュール名は定数「MODULE_NAME」として書けるため、どの関数でも使い回せます。


```vba:FileReader.bas
' サンプル用関数
' 関数の先頭、末尾にエラー処理を付与
'
Public Sub sample()
    On Error GoTo ErrHdl
    
    ' 存在しないシートを選択
    ThisWorkbook.Worksheets("存在しないシート").Activate
ErrHdl:
    If Err.Number <> 0 Then
        Debug.Print MODULE_NAME　&　".sample", Err.Description
    End If
End Sub
```

エラー処理の構文は辞書登録するか、コピペして使い回すと良いでしょう。
ただし、関数名（この場合 ".sample"）は毎回書き換える必要があるので注意してください。

また、1つの関数の適切な長さについては別の記事で改めて掘り下げますが、保守性の向上のために出来るだけ短く、20行〜30行程度に収まるように分割します。
分割した各関数にはわかり易い名前を付けて、全てにエラー処理を付けます。

　
## 対策 2： エラーをロギング

エラー内容をエラーログに出力できるようにすると更に良いです。
開発者がエラーログを見ることで、どこでどんなエラーが発生したか把握しやすくなります。

以下の例では、エラー処理部分でロギング用の関数を呼び出しています。

```vba:FileReader.bas
' サンプル用関数。
' 関数の末尾でログ出力用関数を呼び出す。
'
Public Sub sample()
    On Error GoTo ErrHdl
    
    ' 存在しないシートを選択
    ThisWorkbook.Worksheets("存在しないシート").Activate
ErrHdl:
    ' ロギング用関数を呼び出し、エラー箇所（モジュール名と関数名）、エラー内容をログ出力する
    If Err.Number <> 0 Then
        Util.logError MODULE_NAME　&　".sample", Err.Description
    End If
End Sub
```

以下はエラーログ出力用関数の例です。
予め自分の使いやすい形に別モジュールで定義しておき、プロジェクトごとに使いまわします。

```vba:Util.bas

' エラーロギング用関数
' エラー情報をログファイル、イミディエイトウィンドウに出力。
' ブック名、メソッド名、指定されたテキストを出力する。
'
' ※出力メッセージの書式 … "2022/07/16 00:00:00  ブック名  メソッド名  エラーメッセージ"
 '
' 引数： methodName String エラー発生メソッド名
'  　　   errMsg          String エラー メッセージ
'
Public Sub logError(ByVal methodName As String, ByVal errMsg As String)
    On Error GoTo ErrHdl
    Dim fPath, txt
    Const ERROR_LOG_FILE_NAME = "errorLog.txt"

    ' 出力先のファイルパスをセット（ブックと同じフォルダ）
    fPath = ThisWorkbook.Path & "¥" & ERROR_LOG_FILE_NAME
    
    '  ログに書き込むテキストをセット
    txt = Format(Now, "YYYY/MM/DD HH:mm:ss") & vbTab & ThisWorkbook.name & vbTab & methodName & "() " & vbTab & errMsg
     
    '  ログファイルに出力
    Open fPath For Append As #1
        Print #1, txt
    Close #1
    
    ' 同内容をイミディエイトウィンドウに出力
    Debug.Print "【Error!!】 " & txt
ErrHdl:
    If Err.Number <> 0 Then
        Debug.Print (MODULE_NAME & ".logError() " & Err.Description)
    End If
End Sub

```

<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200881/df1206d0-3a13-509b-397b-fd3d365eb211.png">


エラーログの出力先はブックと同じフォルダでも良いですし、
ファイルサーバ上に出力して、複数のブックで同じエラーログを共有する形をとっても良いと思います。

エラーログにはブック名とエラー発生箇所、エラー内容が出力されますので、開発者はこれを見ればエラー内容が把握できます。

また、ロギング関数ではWindowsのユーザ名を一緒に出力するなど、ご自分が使いやすいように色々工夫してみてください。


## その他

上記の形を取ることで、エラー発生時にも処理が止まらず、実行時エラーウィンドウも表示されなくなりました。

無視しても良い軽微なエラーの場合はそのまま処理が進むようにし、
重大なエラーの可能性がある箇所ではエラー処理内部でフラグをオンにして、呼び出し元で処理を止めるなどすれば良いと思います。

また、メッセージボックスを使って、ユーザ側に分かりやすいエラーメッセージを表示しても良いでしょう。

<strong>【軽微なエラーの例】</strong>
　100件のファイルを読み込む場合、そのうち1件のファイルが壊れていたら無視して次に進む

<strong>【重大なエラーの例】</strong>
　DBに接続できない場合、ユーザ側にエラーメッセージを表示して処理を止める


## まとめ

:::note info
エラー処理のまとめ
- 実行時エラーウィンドウは表示させないようにする
- すべての関数にはエラー処理を付け、関数はできるだけ細かく分割する
- エラー処理ではエラーログを出力できるとなお良い
- エラーメッセージを表示したい場合、開発者側で分かりやすいメッセージを表示する
:::