- エラハンドリングのベストプラクティスが知りたい!
このような疑問にお答えします。
Excel VBAって手軽にプログラムを組める一方で、エラーハンドリングをきちんと組んであるコードが少ない印象を受けています。
ただ、いざエラー処理を追加しようと思っても「一体どうやって組めば良いエラー処理ができるのか…」と途方に暮れてしまう方も多いと思います。
On Error GoToを使えば良い!といった記事は見つかりますが、次のような疑問への解決になっていないものがほとんどです。
- エラーの種類ごとに挙動を変える方法は?
- カスタムエラーを定義したいのだが…
- どんなエラーメッセージを設定したら良い?
本記事の内容が「答え」とまでは言いませんが、僕が普段VBAコードを組む際の考え方をお伝えできたらと思います。
エラーハンドリングについて漠然とした不安がある方は、ぜひ参考にしてください。
開発中の原因調査はDebug.Print+イミディエイトウィンドウで十分
本番向けのエラー処理を組む前段階として、開発中に「なぜ想定通り動かないのか」を調べる作業があります。
ここは凝ったデバッグツールを持ち出さなくても、Debug.Printとイミディエイトウィンドウの組み合わせで大抵は事足ります。僕もプロジェクトによって使うツールは変えますが、原因調査の第一手はいつもこれです。
変数の中身をイミディエイトウィンドウに流す
怪しい変数の値をその場でDebug.Printに渡すだけで、ブレークポイントを仕掛けなくても処理の流れを追えます。
Sub CheckValue()
Dim i As Long
For i = 1 To 5
Debug.Print "i = " & i & " / result = " & i * 10
Next i
End SubイミディエイトウィンドウはCtrl + Gで開けます。出力が溜まってきたらウィンドウ内で全選択して削除すれば、まっさらな状態に戻せます。
どこからの出力か分かるように目印を付ける
複数のプロシージャで同時にDebug.Printを仕込むと、どの出力がどこから来たのか分からなくなりがちです。プロシージャ名をプレフィックスに付けておくと、後から見返したときに追いやすくなります。
Debug.Print "[ImportData] row=" & i & " status=" & statusここまでは、開発中に自分で原因を追うための話です。実際に配布・運用するマクロでは、こうした場当たり的な確認だけでなく、構造立ったエラー処理が必要になります。
エラーハンドリングのベストプラクティス
質の高いエラーハンドリングを行うまでの流れは以下の通りです。
- カスタムエラーの定義
- エラーを発生させる
- エラー処理を構造化
- ログ記録をする
- 上位モジュールに伝播させる
- クリーンアップ処理
- ユーザーにエラー内容を伝える
一つ一つは大して難しくないので、順を追って説明していきます。
カスタムエラーの定義
VBAではデフォルトでもエラーが定義されていますが、それ以外にカスタムでエラーを定義させたい場合があります。
そのような時にはシステム定義のエラー番号(1~65535)との衝突を避けるために、独自の範囲を設定しましょう。
一般的には100000以上の数値を指定することが多いです。
さらに付け加えると、エラーのカテゴリごとに1000区切りなどで番号を振ると管理が楽になります。
' データベース関連のエラー
Public Const ERR_DB_CONNECTION_FAILED As Long = 100000
Public Const ERR_DB_QUERY_FAILED As Long = 100001
' ファイル操作関連のエラー
Public Const ERR_FILE_NOT_FOUND As Long = 101000
Public Const ERR_FILE_READ_FAILED As Long = 101001
' ユーザー認証関連のエラー
Public Const ERR_USER_NOT_AUTHORIZED As Long = 102000
Public Const ERR_USER_NOT_FOUND As Long = 102001
' ネットワーク関連のエラー
Public Const ERR_NETWORK_TIMEOUT As Long = 103000
Public Const ERR_NETWORK_UNREACHABLE As Long = 103001エラーを発生させる
Err.Raiseメソッドを使ってカスタムエラーを発生させます。
Err.Raise ERR_DB_CONNECTION_FAILED, "ConnectDatabase", "データベースへの接続に失敗しました。"引数の内容は以下の通りです。
| 引数 | 引数の内容 |
|---|---|
| 第1引数 | エラー番号 |
| 第2引数 | エラーの発生源 |
| 第3引数 | エラーの説明文 |
Errオブジェクトの内容にあたる部分を作っていく工程になります。
エラー処理を構造化
Err.Raiseで発生させたエラーを補足するためのコードを追加します。
やることは二段階あります。
On Error GoToでエラーハンドラーを設定Select Caseでエラーごとに処理を変える
具体的なコード例を挙げます。
Sub ConnectDatabase()
On Error GoTo ErrorHandler
' データベースエラーを発生させる
Err.Raise ERR_DB_CONNECTION_FAILED, "ConnectDatabase", "データベースへの接続に失敗しました。"
Exit Sub
ErrorHandler:
Select Case Err.Number
Case ERR_DB_CONNECTION_FAILED
MsgBox "ERR_DB_CONNECTION_FAILED: " & Err.Description
Case ERR_DB_QUERY_FAILED
MsgBox "ERR_DB_QUERY_FAILED: " & Err.Description
Case Else
MsgBox "予期せぬエラー: " & Err.Description
End Select
Err.Clear
End Sub上記のCase ElseではDescriptionを定義していないのでは?と思われるかもしれませんが、問題なしです。
というのもCase Elseに入る場合はシステムエラーの場合なので、システム側で用意されたエラーメッセージが自動的に入るためです。
もちろん、カスタムエラーならErr.Raiseの第二引数で詳細を指定しているので、こちらも問題なしですね。
ログ記録をする
エラーの詳細情報をテキストファイルに書き出しておくと、本番運用中に起きたエラーを後から追えます。
Sub WriteErrorLog(ByVal errNumber As Long, ByVal errSource As String, ByVal errDescription As String)
Dim logPath As String
Dim fileNum As Integer
logPath = ThisWorkbook.Path & "\error_log.txt"
fileNum = FreeFile
Open logPath For Append As #fileNum
Print #fileNum, Format(Now, "yyyy-mm-dd hh:nn:ss") & vbTab & errNumber & vbTab & errSource & vbTab & errDescription
Close #fileNum
End SubFor Appendで開くことで、既存のログを上書きせずに追記していきます。ErrorHandler側からはCall WriteErrorLog(Err.Number, Err.Source, Err.Description)のように渡すだけで使えます。
上位モジュールに伝播させる
呼び出し元のモジュールがある場合には、必要に応じてErr.Raiseで上位モジュールにエラー内容を伝播させます。
If Err.Number = ERR_DB_CONNECTION_FAILED Then
' エラー処理
Err.Raise Err.Number, Err.Source, Err.Description
End Ifこの場合には、下位モジュールではエラーを上位に伝播させるだけでCase文などは使いません。
そして伝播されたエラーは、上位モジュール側で処理をすることになります。
Sub UpperLevelFunction()
On Error GoTo ErrorHandler
' この関数でエラー発生
Call LowerLevelFunction()
' 正常終了時の処理
MsgBox "処理が正常に完了しました"
Exit Sub
ErrorHandler:
' 上位モジュールでのエラー処理
Select Case Err.Number
Case ERR_DB_CONNECTION_FAILED
MsgBox "DB接続エラーが発生しました: " & Err.Description
Case ERR_DB_QUERY_FAILED
MsgBox "DBクエリエラーが発生しました: " & Err.Description
Case Else
MsgBox "予期せぬエラーが発生しました: " & Err.Description & " (エラー番号: " & Err.Number & ")"
End Select
' エラー情報をクリア
Err.Clear
End Subもちろん、下位モジュールでエラー処理をして上位モジュールにはエラーを伝播させないことも可能です。
Sub LowerLevelFunction()
On Error GoTo ErrorHandler
' 処理
Err.Raise 1001, "LowerLevelFunction", "下位モジュールでのエラー"
Exit Sub
ErrorHandler:
MsgBox "エラー: " & Err.Description
Err.Clear
Exit Sub
End SubErr.Clearすることで、上位モジュールにエラーが伝播しません。
クリーンアップ処理
プロシージャ内で使われたリソースの解放なども、忘れずに行うようにします。
例えば以下のようにExitSubラベルをfinally的な用途で使うことで、サブプロシージャの終了時に必ず実行したい処理を指定できます。
Sub ConnectDatabase()
On Error GoTo ErrorHandler
' 正常終了時の処理
Dim conn As ADODB.Connection
Set conn = New ADODB.Connection
conn.Open "connection string"
GoTo ExitSub
ErrorHandler:
' エラー処理
Resume ExitSub
ExitSub:
If Not conn Is Nothing Then
If conn.State = adStateOpen Then conn.Close
Set conn = Nothing
End If
Exit Subユーザーにエラー内容を伝える
ユーザーにはわかりやすいメッセージを表示します。
MsgBox "データの保存中にエラーが発生しました。ネットワーク接続を確認してください。", vbExclamation, "エラー"実装側だとどうしても専門的な言葉になってしまいがちですが、利用者の立場に立って平易な言葉で内容を伝えるようにしましょう。
まとめ
エラーハンドリングは型を身につけてしまえば、そこまで恐れることもないかと思っています。
開発中はDebug.Printで手早く原因を探り、本番運用ではログをファイルに残す。この使い分けだけでも、エラーとの付き合い方はだいぶ楽になります。
まずは本記事の内容を実践していただき、合わない部分を改善しつつ自分なりのベストプラクティスを作り上げていただけたらと思います。

コメント