VBA でスタックトレース情報を得る方法を整理してみる
はじめに
プログラミングのデバッグでは、スタックトレース(関数の呼び出し履歴)は非常に重要な情報です。
エラーが起きたときにどの関数からどの関数を経由してここに至ったのかが分かれば、原因を突き止めやすくなります。
しかし、VBA では「コード実行中」にスタックトレースを取得する標準的な機能がありません。
VBA の長い歴史の中で様々な手法・ツールが考案されています。
それぞれに特徴があり、プロジェクトの規模(小規模・大規模)や開発体制(個人・チーム)などの前提条件によって向き不向きが変わります。
そこで、本記事では各手法の特徴を整理します。
対象読者
-
VBA 脱初級者 ~ 中級者
- 基本的な用語は解説しないので、詳細は検索 & 参考リンクを読んでください
- 標準機能で得られるデバッグ情報が皆無で苦しんでいる開発者
- 自身のプロジェクトにどの手法が適しているか悩んでいる方
-
他言語経験者でモダンな言語の良さを再確認したい人当たり前にある機能がない恐怖を垣間見たい方
記事の流れ
- スタックトレースに関する背景情報
- 各手法の特徴・適用場面
- 自作している VBA ロガーアドインの宣伝(おまけ)
VBA におけるスタックトレースの課題
本題へ入る前に、VBA、及び、専用エディター(VBE)のスタックトレース機能の仕様や課題について解説します。
興味が無い方は次のセッションまで飛ばしてください。
スタックトレースとは
スタックトレースは、プログラムの実行経路(どの関数からどの関数が呼ばれたか)を示す情報です。
VBA 以外の言語(C#)でのスタックトレース機能
Environment.StackTrace
をコード中で呼び出すと、以下の様な出力が得られます。
StackTrace: ' 場所 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
場所 System.Environment.get_StackTrace()
場所 NameSpace.HogeClass.<Speak>d__2.MoveNext() 場所 FilePath\HogeClass.cs:行 62
場所 System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start[TStateMachine](TStateMachine& stateMachine)
場所 NameSpace.HogeClass.DoSomething(String text)
場所 NameSpace.FugaClass.<SpeakAsync>d__14.MoveNext() 場所 FilePath\FugaClass.cs:行 149
この例だと下から順に読むことで処理の流れを追えます。
- FugaClass の 149 行目にて HogeClass の
DoSomething()
関数が呼び出されている - HogeClass の 62 行目にて
System.Environment.get_StackTrace()
関数が呼び出されている
※ 筆者の開発中のコードで実行したので、ファイル名は加工しています。
VBA のスタックトレース機能
VBA にはコード中でスタックトレースを表示する標準プロシージャが存在しません。
実際、複数の記事で 散々に酷評 VBA の問題点としてスタックトレースが貧弱であり、エラー解決の手間が増大していると指摘されています。
補足:限定的なスタックトレース機能は存在します
VBA のエディター(VBE)には「呼び出し履歴ウィンドウ」という機能があります。
ただし、以下のような制約があります。
-
実行停止時のみ利用可能
- エラー発生で処理が止まった場合は情報が見れない
- 事前のブレークポイント設定が必要
- エディターの GUI 表示のみ
- Debug.Print でログ出力できない
以下の記事では VBE でのスタックトレース情報の確認方法が紹介されています。
スタックトレースの重要性が他言語よりも高い背景事情
VBA 開発では、エラーハンドリングが不十分な場合、エラー発生時の状況把握が困難になるという大きな課題があります。
通常は、以下のような対策を行います。
- エラーハンドラー(
On Error GoTo ErrorHandler
)を記述する-
Debug.Print Err.Number
の様にエラー内容を記録する
-
- 呼び出し元へのエラーの伝播(
Err.Raise
)を記述する - プロシージャの呼び出し・完了時に
Debug.Print
でログを残す
しかし、これらは工数が多く、開発速度優先で後回し・一部のみ実装とされるケースもあります。
対策を怠ると、いつどこでエラーが発生したかの手がかりが皆無な状況へ陥ります。
具体的な例
エラーハンドリングが無い場合、標準機能でエラー発生時に得られる情報を紹介します。
プロシージャ ProcessStepC()
にて 0 除算を行ったとします。
コードを実行すると、以下の様にエラーを知らせるダイアログが表示されます。
デバッグボタンを押すと、エラーが発生した行が黄色にハイライトされます。
--- 得られる情報は以上です。---
致命的なのは、上記の情報が全てエディター(VBE)の GUI で表示されるだけであり、テキストデータ(ログなど)は残らないことです。
無論、情報量も不足しています。
- どのタイミングでエラーが起きたのか?
- どの様な順序でプロシージャが呼び出されていたのか?
といった情報は分かりません。
他の言語と比べ、VBA では対策なしにエラーが発生した際の代償がとても大きいと言えます。
他の言語の場合、エラー時にスタックトレース情報がコンソールにテキストとして出力され最低限の情報が担保されているパターンが多いです。
既存の解決手法
VBA でスタックトレース相当の情報を得るため、これまで様々な手法が開発されてきました。
本項では、5 つの手法の特徴を紹介します。
1. 手作業ログ埋め込み
概要
各プロシージャに Debug.Print
を開発者が手動で挿入する最も原始的かつ直接的な方法です。
Sub MainProc()
Debug.Print "MainProc 開始"
' 処理内容
Debug.Print "MainProc 終了"
End Sub
特徴
- ✅ 利点
- VBA 標準機能のみで実現可能
- 導入コスト・学習コストがゼロ
- 出力内容を完全にコントロール可能
- ❌ 欠点
- 関数追加時に手動でログコード追加が必要
- ヒューマンエラー(書き忘れ・記述ミス)が頻発
- 大規模プロジェクトでは膨大な手作業が必要
適用場面
- ✅ 小規模な一時的コード
- ❌ 大規模プロジェクト・チーム開発
2. エラー伝播による擬似スタックトレース
概要
エラー発生時に自身の情報を追加して、エラーを上位プロシージャに伝播させる方法です。
Sub MainProc()
On Error GoTo ErrorHandler
SubProc
Exit Sub
ErrorHandler:
Err.Description = "MainProc > " & Err.Description
Debug.Print Err.Description
Err.Raise Err.Number
End Sub
Private Sub SubProc()
On Error GoTo ErrorHandler
' エラーが発生する処理
Dim myValue As Integer
myValue = 1 / 0
Exit Sub
ErrorHandler:
Err.Description = "SubProc > " & Err.Description
Err.Raise Err.Number
End Sub
MainProc > SubProc > 0 で除算しました。
エラー伝播方法の技術的補足
On Error GoTo ErrorHandler
これにより、エラーが発生した際に ErrorHandler
が実行されます。
ErrorHandler:
このコード以下に書かれた内容が、エラー時に実行されます。
Err
このオブジェクトにアクセスすることで、エラー発生時の情報を取得できます。
ErrorHandler
、Err
の詳細については下記公式リファレンスをご確認ください。
特徴
- ✅ 利点
- VBA 標準機能のみで実現
- エラー発生時に呼び出し経路を取得可能
- 通常時のパフォーマンスへの影響が少ない
- ❌ 欠点
- エラー時のみ有効(正常フローは追跡不可)
- 全プロシージャにエラーハンドリングコードが必要
- エラー処理コードの管理が煩雑
適用場面
- ✅ 小規模な一次的コード
- ❌ 正常フローのデバッグが必要な場面・大規模プロジェクト・チーム開発
3. CallByName ラッパー手法
概要
CallByName
関数を利用し、プロシージャ呼び出しをログ出力機能を持つラッパー関数経由で実行する方法です。
CallByName とは?
文字列名を使用してプロパティやメソッドを呼び出す関数です。
CallByName オブジェクト, "メソッドの名前", 呼び出しの種類, [引数]
サンプルコード、及び、実行結果は以下の記事より引用しました。
' Call CallSubByName(クラスのインスタンス, "プロシージャ名", "プロシージャ引数")
Call CallSubByName(vTest1, "Sub1Param", "p1")
2023/09/18 15:37:09.572 START: TestTarget#Sub1Param(VbMethod)
パラメータ1つ p1
2023/09/18 15:37:09.576 FINISH: TestTarget#Sub1Param(VbMethod)
特徴
- ✅ 利点
- 関数呼び出し時に自動でログ出力
- 統一されたログ形式
- 後付けでの既存コードへの導入が容易
- ❌ 欠点
- 標準モジュールの関数を扱う場合は、別途アダプタークラスが必要
- 通常の関数呼び出しと記述方法が異なり可読性が低下
- 階層構造のスタックトレース情報がない
適用場面
- ✅ クラスベースの設計が確立している場合
- ❌ 標準モジュール中心の既存コード
- ❌ 階層構造のスタックトレース情報が必要な場合
4. マクロ実行でのログ出力コードの自動挿入
概要
VBA マクロを実行し、対象プロシージャの最初と最後にログ出力コードを自動的に挿入する方法です。
特徴
- ✅ 利点
- マクロ実行により一括でコード挿入が可能
- 階層構造のスタックトレース情報を取得可能
- 元のコード構造を大きく変更せずに導入
- ❌ 欠点
- VBA プロジェクトへのプログラムによるアクセス許可設定が必要
- マクロでコードを直接書き換えるため
- 企業環境ではセキュリティポリシー上、制限される可能性がある
- 意図しないコード変更のリスク
- 新規関数作成の度にマクロ実行が必要
- VBA プロジェクトへのプログラムによるアクセス許可設定が必要
適用場面
- ✅ 個人開発環境での使用
- ✅ セキュリティ制約のない環境
- ❌ 企業環境での使用
5. vbWatchdog
概要
包括的なエラーハンドリングとデバッグ支援を提供する高機能な有償アドインです。
- ✅ 利点
- 詳細なエラーレポート、リアルタイム変数監視など高機能
- 商用製品としての安定性と信頼性
- インストール後すぐに利用可能
- ❌ 欠点
- $260 の導入コスト
- 豊富な機能を活用するための学習コスト
- 主にエラー発生時の情報収集に特化
適用場面
- ✅ 予算に余裕のあるプロジェクト
- ✅ 包括的なエラー管理が必要な場合
- ❌ 小規模開発・個人プロジェクト
- ❌ コード実行中に階層構造のスタックトレース情報を得たい場合
既存手法比較表
スタックトレース
手法 | 取得タイミング | 階層情報 |
---|---|---|
手作業ログ | ⭕任意 | 可能(手動実装) |
エラー伝播 | ❌エラー時のみ | ⭕あり |
CallByName | プロシージャ・プロパティの開始・終了 | なし |
VBE 自動挿入 | ⭕任意 | ⭕あり |
vbWatchdog | ❌エラー時のみ | ⭕あり |
導入・運用面
手法 | コスト | ライセンス | 工数 |
---|---|---|---|
手作業ログ | ⭕無料 | N/A | ❌大 |
エラー伝播 | ⭕無料 | N/A | ❌大 |
CallByName | ⭕無料 | N/A | 中 |
VBE 自動挿入 | ⭕無料 | ⭕OSS(MIT) | ⭕小 |
vbWatchdog | ❌$260 | 商用 | ⭕小 |
総評
「小規模で個人利用、短期間だけ使いたい」
→ 手作業ログ、エラー伝播
「大規模で長期運用・チーム開発」
→ VBE自動挿入、vbWatchdog
「既存資産への組み込みやすさ重視」
→ VBE自動挿入
「無料・すぐ試せるもの」
→ 手作業ログ、エラー伝播
自作している VBA ロガーアドインの宣伝(おまけ)
企業での複数人チームによる大規模プロジェクトを想定した場合、以下が必要不可欠です。
- 階層情報を含むスタックトレースをコード中の任意の場所で取得し、挙動を詳細に把握
- シンプルな導入・使用方法による学習コスト削減
- 明確なライセンス
- 要件に応じた機能拡張(出力先、ログ要素のカスタマイズ)
- 保守しやすい設計(デバッグ容易性、拡張性)
しかし、これらすべてを理想的に満たす手法は存在しないと感じました。
そこで、自身にとっての理想のロガーを自作しました。
vba_stack_trace_logger について
実際のログ出力結果、及び、使用したコードは以下の通りです。
[2025-06-22 19:30:24.076][Trace][MyModule.MainProc] >> Enter MyModule.MainProc
[2025-06-22 19:30:24.077][DEBUG][MyModule.MainProc] This is my message
[2025-06-22 19:30:24.079][Trace][MyModule.SubProc < MyModule.MainProc] >> Enter MyModule.SubProc
Option Explicit
Private Const MODULE_NAME As String = "MyModule"
Sub CheckLogger()
' === ロガーの初期化 ===
myLogger.StartConfiguration _
.EnableStackTrace _
.EnableWriteToExcelSheet _
.SetOutputExcelSheet(ActiveSheet) _
.Build
' === ログの出力 ===
myLogger.Log "Start"
MainProc
myLogger.Log "End"
' === ロガーの終了処理 ===
myLogger.Terminate
End Sub
Private Sub MainProc()
Const PROC_NAME As String = "MainProc": Dim scopeGuard As Variant: Set scopeGuard = myLogger.UsingTracer(MODULE_NAME, PROC_NAME)
myLogger.Log "This is my message", LogTag_Debug
SubProc
End Sub
Private Sub SubProc()
Const PROC_NAME As String = "SubProc": Dim scopeGuard As Variant: Set scopeGuard = myLogger.UsingTracer(MODULE_NAME, PROC_NAME)
Dim myID As Long: myID = 1234
myLogger.Log "My ID is " & myID, LogTag_Warning
End Sub
本ロガーはアドインとして作成しています。
ライセンスは MIT、つまり OSS で公開しています。
中身は OOP(オブジェクト指向プログラミング)を活用しており、複雑ですが、利用者は MyLogger
というロガー専用のキーワードのみで全ての機能にアクセスできます。
OOP 経験者が機能拡張しやすいように作成したつもりです。
・ロガーアドインのクラス図(参考までに)
Discussion