1. なぜWord VBAのログ設計を甘く見てはならないのか
多くのVBA開発者は、Excel VBAの延長線上でWord VBAを書き始めます。そして、運用フェーズに入った瞬間に地獄を見るのです。
WordはExcelとは本質的に異なるオブジェクトモデルを持っています。二次元グリッドでデータが静的に配置されているExcelに対し、Wordは「流動的なテキストストリーム」です。ページの概念はレンダリングエンジンが動的に計算しており、Rangeオブジェクトの範囲(Start / End)は文字の挿入や削除によってリアルタイムに伸縮します。
このような動的かつ不安定な環境で、以下のような「お粗末なエラーハンドリング」を放置するとどうなるでしょうか。
‘ 絶対にやってはいけない、破滅を招く典型例
On Error Resume Next
Doc.Bookmarks(“Target”).Range.Text = “Data”
‘ エラーが発生しても無視され、不正な状態で処理が進行する
「エラーが起きても止まらないマクロ」は、エンドユーザーにとって一見親切に見えます。しかしその実態は、ドキュメントをサイレントに破損させ、原因究明を不可能にする最悪の爆弾です。
実務で耐えうるVBAツールを構築するためには、予期せぬエラーを確実にトラップし、「いつ、誰の、どの文書の、どのモジュールの、どの処理で、なぜ落ちたのか」を、管理者が一目で特定できる堅牢なロギング機構が不可欠です。本稿では、FileSystemObject(FSO)を極限までチューニングし、Word特有のコンテキスト情報をも補足する、プロ仕様のエラーロギング基盤の設計・実装を伝授します。
—
2. 堅牢なエラーログ基盤を支える「3つの設計原則」
コードを書く前に、アーキテクチャの骨子を定義します。私が率いるプロジェクトでは、VBAのロギングにおいて以下の3原則を厳守させています。
原則1:配布の容易性を担保する「遅延バインディング(Late Binding)」の採用
開発時は `Microsoft Scripting Runtime` への参照設定(早期バインディング)を行い、IntelliSense(入力補完)の恩恵を受けるべきです。しかし、一般ユーザーに配布する本番コードでは、参照設定の破損(Reference Broken)によるコンパイルエラーを防ぐため、遅延バインディング(`CreateObject`)に切り替えます。これにより、環境依存によるマクロの起動不全を根絶します。
原則2:共有フォルダでの衝突を防ぐ「競合回避とリトライアルゴリズム」
ログファイルをネットワーク上の共有フォルダに集約する場合、複数ユーザーによる同時書き込み競合(ファイルロック)が必ず発生します。ログ書き込み処理には、ミリ秒単位のウェイトを挟んだリトライループを実装し、書き込み失敗によるシステムクラッシュを防止しなければなりません。
原則3:Wordのコンテキスト(文脈)をログに強制注入する
単に `Err.Number` と `Err.Description` を吐き出すだけのログは三流です。Word VBAのデバッグにおいて本当に必要なのは、以下の情報です。
- アクティブな文書名(`ActiveDocument.Name`)
- エラー発生時のカーソル位置(セレクションの文字位置、または段落番号)
- 実行していたサブルーチン(コールスタックの疑似再現)
—
3. 【実践】プロダクション品質のLoggerクラスと実装例
それでは、実戦にそのまま投入できるコードを実装します。
この構成は、ログ出力をカプセル化するクラスモジュール `clsLogger` と、それを呼び出して業務ロジックを実行する標準モジュール `modMain` の2レイヤーで構成されます。
3.1. クラスモジュール:`clsLogger`
クラスモジュールを挿入し、オブジェクト名を `clsLogger` に変更して以下のコードを貼り付けてください。
Option Explicit
‘ ==============================================================================
‘ クラス名: clsLogger
‘ 概要: 堅牢なファイルロギングを提供するユーティリティクラス(遅延バインディング仕様)
‘ ==============================================================================
Private Const ForAppending As Long = 8
Private Const MAX_RETRIES As Long = 5
Private Const RETRY_DELAY_MS As Long = 100
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If
Private m_LogFolderPath As String
Private m_LogFileName As String
‘ ——————————————————————————
‘ 初期化処理
‘ ——————————————————————————
Private Sub Class_Initialize()
‘ デフォルトのログ出力先をマクロ有効テンプレート/文書と同一フォルダに設定
m_LogFolderPath = ThisDocument.Path
m_LogFileName = “vba_execution_error.log”
End Sub
‘ ——————————————————————————
‘ プロパティ設定
‘ ——————————————————————————
Public Property Let LogFolderPath(ByVal value As String)
m_LogFolderPath = value
End Property
Public Property Let LogFileName(ByVal value As String)
m_LogFileName = value
End Property
‘ ——————————————————————————
‘ メインメソッド: エラーログの書き込み
‘ ——————————————————————————
Public Sub WriteErrorLog( _
ByVal procedureName As String, _
ByVal errObj As ErrObject, _
Optional ByVal extraInfo As String = “”)
Dim fso As Object
Dim logStream As Object
Dim logPath As String
Dim logMessage As String
Dim retryCount As Long
Dim writeSuccess As Boolean
On Error GoTo Catch_Logging_Error
‘ ログフルパスの組み立て
If Right(m_LogFolderPath, 1) <> “\” Then
logPath = m_LogFolderPath & “\” & m_LogFileName
Else
logPath = m_LogFolderPath & m_LogFileName
End If
‘ FSOのインスタンス化(遅延バインディング)
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ ログフォルダーが存在しない場合は自動生成
If Not fso.FolderExists(m_LogFolderPath) Then
fso.CreateFolder m_LogFolderPath
End If
‘ ログメッセージの構築(Wordのコンテキストを内包)
logMessage = BuildLogMessage(procedureName, errObj, extraInfo)
‘ ファイル書き込み(競合を考慮したリトライループ)
retryCount = 0
writeSuccess = False
Do While (Not writeSuccess) And (retryCount < MAX_RETRIES)
On Error Resume Next
Set logStream = fso.OpenTextFile(logPath, ForAppending, True)
If Err.Number = 0 Then
logStream.WriteLine logMessage
logStream.Close
writeSuccess = True
Else
' ファイルがロックされている場合はウェイトを入れてリトライ
retryCount = retryCount + 1
Sleep RETRY_DELAY_MS
End If
On Error GoTo Catch_Logging_Error
Loop
If Not writeSuccess Then
' すべてのリトライが失敗した場合、イミディエイトウィンドウとMsgBoxで警告
Debug.Print "【致命的エラー】ログファイルの書き込みに失敗しました。パス: " & logPath
End If
CleanUp:
Set logStream = Nothing
Set fso = Nothing
Exit Sub
Catch_Logging_Error:
' ロギング自体がクラッシュした場合のセーフティネット
MsgBox "ロギングシステム内で深刻なエラーが発生しました。" & vbCrLf & _
"Error: " & Err.Description, vbCritical, "Fatal Error in Logger"
Resume CleanUp
End Sub
' ------------------------------------------------------------------------------
' ヘルパーメソッド: ログメッセージの生成
' ------------------------------------------------------------------------------
Private Function BuildLogMessage( _
ByVal procedureName As String, _
ByVal errObj As ErrObject, _
ByVal extraInfo As String) As String
Dim sb As String
Dim activeDocName As String
' Word特有のコンテキスト情報の取得(ドキュメントが開かれていない場合を考慮)
On Error Resume Next
If Documents.Count > 0 Then
activeDocName = ActiveDocument.Name
Else
activeDocName = “No Document Opened”
End If
On Error GoTo 0
sb = “[” & Format(Now, “yyyy-MM-dd HH:mm:ss”) & “] ”
sb = sb & “[USER: ” & Environ(“USERNAME”) & “] ”
sb = sb & “[DOC: ” & activeDocName & “] ”
sb = sb & “[PROC: ” & procedureName & “]” & vbCrLf
sb = sb & ” -> ERROR NUMBER: ” & errObj.Number & vbCrLf
sb = sb & ” -> SOURCE : ” & errObj.Source & vbCrLf
sb = sb & ” -> DESCRIPTION : ” & errObj.Description & vbCrLf
If extraInfo <> “” Then
sb = sb & ” -> CONTEXT : ” & extraInfo & vbCrLf
End If
sb = sb & “——————————————————————————–”
BuildLogMessage = sb
End Function
3.2. 標準モジュール:`modMain`(業務ロジック実行部)
標準モジュールを挿入し、以下のコードを貼り付けます。これは、堅牢な `Try-Catch` 構造を模倣した業務コードのテンプレートです。
Option Explicit
‘ ==============================================================================
‘ モジュール名: modMain
‘ 概要: 業務自動化マクロのエントリーポイントとエラーハンドリングの実装例
‘ ==============================================================================
Public Sub ExecuteWordTask()
‘ 1. Loggerのインスタンス生成
Dim Logger As clsLogger
Set Logger = New clsLogger
‘ 必要に応じて出力先やファイル名を変更可能(デフォルトはThisDocument.Path)
‘ Logger.LogFolderPath = “C:\VBA_Logs”
‘ Logger.LogFileName = “word_macro_errors.txt”
‘ 2. 構造化エラーハンドリングの宣言
On Error GoTo ErrorHandler
‘ — [業務ロジック開始] —
Debug.Print “処理を開始します…”
Dim doc As Document
Set doc = ActiveDocument ‘ 文書が開いていない場合はここでErr.Number = 4248が発生する
‘ Word特有の危険な操作:存在しないブックマークへのアクセス(わざとエラーを発生させる)
Dim rng As Range
Set rng = doc.Bookmarks(“NonExistentBookmark”).Range
rng.Text = “魂のログ出力をここに書き込む”
‘ — [業務ロジック終了] —
NormalExit:
‘ 正常終了時のクリーンアップ処理(ゾンビプロセス化の防止)
Set rng = Nothing
Set doc = Nothing
Set Logger = Nothing
Debug.Print “処理が正常に終了しました。”
Exit Sub
ErrorHandler:
‘ 3. エラーハンドリングとコンテキストの抽出
Dim contextInfo As String
contextInfo = “Failed during Bookmark range assignment.”
‘ Word特有の状況証拠を追加(現在選択されている文字位置など)
On Error Resume Next
If Selection.Type <> wdSelectionIP Then
contextInfo = contextInfo & ” (Selection Start: ” & Selection.Start & “, End: ” & Selection.End & “)”
End If
On Error GoTo ErrorHandler ‘ ハンドラーを元に戻す
‘ Loggerクラスに例外情報を引き渡し、ログファイルへ物理書き込み
Logger.WriteErrorLog “modMain.ExecuteWordTask”, Err, contextInfo
‘ 4. エラーのユーザー通知(実務ではサイレントにするか選択)
MsgBox “処理中に予期せぬエラーが発生しました。” & vbCrLf & _
“詳細はログファイルをご確認ください。” & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “エラー発生”
‘ 5. 異常終了時も必ずクリーンアップを通す
Resume NormalExit
End Sub
—
4. ディープダイブ:なぜこの実装なのか?コード解説とパフォーマンスの重み
プロフェッショナルが書くコードには、1行たりとも「なんとなく書かれたコード」は存在しません。上記のコードに隠された、設計上の重要な「意図」を解説します。
① `ForAppending` (8) による追記モードの絶対性
FSOでファイルを操作する際、ファイルを新規作成して上書きする `ForWriting` (2) ではなく、必ず `ForAppending` (8) を指定します。
これは、過去に発生したエラー履歴を破壊しないためです。また、`OpenTextFile` の第3引数(`Create`)を `True` に設定することで、「ファイルが存在しなければその場で自動生成し、存在すれば追記する」という挙動を1行で担保しています。
② Windows API `Sleep` を用いたロック競合回避
ネットワークドライブ上の共有フォルダにログを出力する場合、複数ユーザーがミリ秒単位で同時にマクロを実行すると、FSOの書き込み時に `Permission Denied (Error 70)` が発生します。
これを回避するため、`Sleep` APIを用いて100ミリ秒の待機時間を設け、最大5回まで再試行(リトライ)するアルゴリズムを組み込んでいます。VBA標準の `Application.Wait` は1秒単位でしか制御できず、UIスレッドを完全にフリーズさせるため、APIによるミリ秒制御がベストプラクティスです。
③ ゾンビプロセス(Word.Application)の発生防止
Word VBAにおいて最も頻出するバグが、エラー発生時にオブジェクトがメモリ上に残り続ける「ゾンビプロセス化」です。
上記の `modMain` では、エラーが発生した場合でも必ず `ErrorHandler` から `Resume NormalExit` へジャンプさせ、すべてのオブジェクト変数(`doc`, `rng`)に `Nothing` を代入して明示的にメモリを解放する堅牢なクリーンアップ構造(他言語における `try-catch-finally` の `finally` 相当)を構築しています。
—
5. システム運用を成功に導くアーキテクトの視点
業務自動化ツールにおける「エラーログ」とは、単なる不具合の記録簿ではありません。それは「システムの健康診断書」であり、「開発者と現場ユーザーを繋ぐ信頼の架け橋」です。
マクロが動かなくなったとき、ユーザーから「なんかエラーが出ました」という曖昧な報告しか受け取れない開発組織は、解決までに多大な時間(コスト)を浪費します。
今回構築したログ基盤があれば、管理者は「〇〇さんが、A契約書.docxを処理している最中に、12行目のブックマーク挿入処理で、書き込み権限エラー(Error 70)を起こした」という客観的事実を、数秒で特定できます。
この堅牢なロギング機構をあなたのアセット(武器)として組み込み、Word VBA開発の品質をエンタープライズレベルへと引き上げてください。コードの美しさと堅牢さは、そのままシステムの寿命へと直結するのです。
