VBAは「Try-Catch」ができない!?
「!?」なんて言ってますが、もちろんできないです。(笑)
ちなみに、VB.NETならできます。
ただ、Try-Catchがあろうなかろうが、正しくエラーハンドリングができるかどうかは別です。
大事なのは、モジュールを構造化したときに、
正しい経路で、エラーを伝播できるか
だと思います。
ということで、VBAにおけるエラーハンドリングについて説明していきます。
何もしなかったらどうなる?
こんな処理を考えてみましょう。
' こいつは上位
Sub Main()
ErrorMethod
MsgBox "完了しました。"
End Sub
' こいつは下位
Sub ErrorMethod()
Err.Raise 513, "なにか起きた"
End Sub
このとき、Mainが上位、ErrorMethodが下位になります。
下位モジュールでエラーが起きてますね。
ちなみに、Err.Raise
は実行時エラーを強制的に発生させるメソッドです。-> Err.Raise
この場合、ErrorMethodの中で実行が止まります。
上位モジュールでエラーハンドリングをしていないからです。つまり
何もしないと、エラーは上位に伝播する
ということになります。
エラーハンドリングに使えるのは、下記のステートメントです。
On Error Goto ラベル名
On Error Resume Next
の2つがあります。
違いは、下記の2点です。
-
On Error Goto
は、指定したラベルの位置まで処理が飛ぶ。 -
On Error Resume Next
は、エラーを無視して突き進む。
では、エラーハンドリングの例を見ていきます。
上位モジュールでのエラーハンドリング
まずは、上位モジュールにおけるエラーハンドリングをしてみましょう。
下記の例は、On Error Goto
の方式です。
Sub Main()
On Error GoTo OnError
ErrorMethod
MsgBox "完了しました。"
Exit Sub
OnError:
MsgBox "なにか起きたらしい"
End Sub
ErrorMethod
が呼び出された時点で、OnErrorラベルに処理が飛んでくれます。
On Error Resume Next
を用いる場合はこうも書けます。
Sub Main()
On Error Resume Next
ErrorMethod
If Err.Number = 0 Then
MsgBox "完了しました。"
Else
MsgBox "なにか起きたらしい"
End If
End Sub
このとき下記のように、後続処理を追加すると勝手に流れてしまう危険性があります。
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も必要なければ書かないですよね)
Sub ErrorMethod()
Err.Raise 513, "なにか起きた"
End Sub
ですが、ファイルIO等の処理を書いている際、
予期せずエラーが発生してしまうことがあります。
その際に、ファイルを閉じる処理(Close)を行わないと、ファイルが開きっぱなしになってしまいます。
そこで、下記のようにコードを直してあげる必要があります。
Sub ErrorMethodWithIO()
On Error GoTo Dispose
Open "path\to\file" For Input As #1
Err.Raise 513, "ファイル読み込み中に、なにか起きた"
Dispose:
' エラーになったときも、必ず閉じる
Close #1
End Sub
こうしてあげると、ファイルが閉じない心配はありません。
では、このモジュールを上位から呼び出すとどうなるでしょうか。
Sub Main()
On Error GoTo OnError
ErrorMethodWithIO
MsgBox "完了しました。"
Exit Sub
OnError:
MsgBox "なにか起きたらしい" & vbCrLf & Err.Number
End Sub
実行すると分かりますが、「完了しました。」とでます。
つまり、下位モジュールのエラーは握り潰されてしまったということです。
それではエラーが起きているのに、原因がわかりません。
(または、エラーが起きていることにすら気づけない。)
これを防ぐために、下位のモジュールでのエラーハンドリングの際は、必ずエラーを再度起こしてあげる必要があります。
その際つかえるのが、Err.Raise
です。
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があろうなかろうが、正しくエラーハンドリングができるかどうかは別です。
というのは、こういうことなのでした。
さて、もう1つ注意すべき点があります。
もう一度コードに修正を入れてみましょう。
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
を読み込む直前に移動します。
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を利用している場合、保守ができなくなり負債がたまって崩壊するときが来るかもしれません。
なので、そういう方もモジュールの構造化や、エラーハンドリングについて、この記事を機に考え直して貰えるなら、書いた甲斐があるというものです。
(簡単なものなら、ビジネスロジックとデータアクセスを分けるとかその程度でもいい。階層ごとに責務が分けられれば正直どんなでもいい。)
そして、そうした積み重ねが、後々の保守や開発効率に寄与するのは言うまでもないと思います。
自分、そして次に直す人のため、安全なエラーハンドリングを心がけていきましょう。