08. エラー処理とデバッグ

try/catch を書いたのに捕まらない — その理由から始める

📅 作成: 2026-08-23 / 更新: 2026-08-23 / 対象: Windows PowerShell 5.1 + PowerShell 7

この章で何ができるようになるか

try/catch で囲んだのに止まらない」は PowerShell では正常な動作です。
バグではありません。PowerShell には他言語にない「非終了エラー」という区分があり、既定ではこれが catch に入らないからです。この仕組みを知らないまま書くと、エラー処理をしたつもりで何も守れていないコードになります。

08.1 2種類のエラー

まず動かして確かめる

try {
    Get-ChildItem 'X:\ないパス'
    'ここまで到達した'
} catch {
    'catch に入った'
}

結果は 「ここまで到達した」 です。画面には赤いエラーが表示されるのに、catch には入りません。

非終了エラー Non-terminating 終了エラー Terminating 赤いメッセージは出る 処理はそのまま続く catch に入らない 例: 存在しないパス、アクセス拒否 赤いメッセージが出る 処理がそこで止まる catch に入る 例: throw、構文エラー、型変換の失敗 なぜ2種類あるのか — シェルだから 1000 個のファイルを処理して1つ失敗したとき、全部止めるより残りを続けたい場面が多い
図 08.1 — 既定では非終了エラーは catch に入らない。バグではなく仕様
この設計はシェルとして自然です。
1000 個のファイルをコピー中に 1 個が読み取り不可だったとき、そこで全部止めるべきでしょうか。多くの場合は「その1件を報告して残りを続ける」ほうが望まれます。
プログラミング言語なら例外を投げる場面でも、シェルは続行を選ぶ — それが非終了エラーです。

どちらになるかは決まっている

発生源種類catch に入るか
Cmdlet が報告するエラー非終了入らない
Write-Error非終了入らない
throw終了入る
構文エラー・型変換の失敗終了入る
.NET メソッドの例外終了入る
外部コマンド(exe)の失敗エラーですらない入らない(08.3)

解決策 — -ErrorAction Stop

非終了エラーを終了エラーに格上げすると catch できます。

try {
    Get-ChildItem 'X:\ないパス' -ErrorAction Stop
    'ここまで到達した'
} catch {
    'catch に入った: ' + $_.Exception.GetType().Name
}
# → catch に入った: DriveNotFoundException
画面表示 処理 $Error Stop 出る 止まる(catch 可) 残る Continue(既定) 出る 続く 残る SilentlyContinue 出ない 続く 残る Ignore 出ない 続く 残らない SilentlyContinue は「隠すが記録は残す」。Ignore は「なかったことにする」
図 08.2 — SilentlyContinueIgnore の違いは $Error に残るかどうか

スクリプト全体を厳しくする

$ErrorActionPreference = 'Stop'      # スクリプトの先頭に書く
業務スクリプトでは先頭に書くことを推奨します。
毎回 -ErrorAction Stop を書くのは現実的ではありません。先頭で一括指定し、続行したい箇所だけ個別に -ErrorAction SilentlyContinue を付けるのが実務的です。
「失敗したのに最後まで走り切って、壊れたデータを出力する」という最悪の結果を防げます。
PowerShell が初めての人へ
Java・C# では例外は必ず catch できるのが前提でした。PowerShell ではcatch できるかどうかが呼び出し側の指定で変わります
戸惑うポイントですが、逆に言えば「止めるか続けるか」を呼ぶ側が決められるということです。同じコマンドを、あるときは厳格に、あるときは寛容に扱えます。

08.2 try / catch / finally

基本の形

try {
    $data = Get-Content .\config.json -Raw -ErrorAction Stop
    $config = $data | ConvertFrom-Json
}
catch [System.IO.FileNotFoundException] {
    Write-Host "設定ファイルがありません"
}
catch {
    Write-Host "予期しないエラー: $($_.Exception.Message)"
}
finally {
    Write-Host "後始末(成功・失敗にかかわらず必ず実行)"
}
try 成功 終了エラー そのまま次へ catch finally — 必ず通る
図 08.3 — finally は成功・失敗のどちらでも実行される。後始末に使う

エラーオブジェクトから取れる情報

catch の中では $_ にエラーの詳細が入っています。実際に取得できる値です。

プロパティ実際の値の例用途
$_.Exception.MessageCannot find drive. A drive with the name 'X' does not exist.人が読むメッセージ
$_.Exception.GetType().NameDriveNotFoundException種類で分岐する
$_.CategoryInfo.CategoryObjectNotFound大まかな分類
$_.FullyQualifiedErrorIdDriveNotFound正確な識別子
$_.InvocationInfo.ScriptLineNumber17発生した行番号
$_.ScriptStackTrace呼び出し履歴どこから呼ばれたか
catch {
    Write-Host "エラー: $($_.Exception.Message)"
    Write-Host "場所  : $($_.InvocationInfo.ScriptName) の $($_.InvocationInfo.ScriptLineNumber) 行目"
}
ログには行番号を必ず含めてください。
メッセージだけでは、同じ処理が複数箇所にあるときどこで失敗したか分かりません$_.InvocationInfo.ScriptLineNumber を1つ足すだけで、調査時間が大きく変わります。

throwWrite-Error の使い分け

throwWrite-Error
エラーの種類終了エラー非終了エラー
処理止まる続く
catch入る入らない
使いどころ続行不可能な状態1件失敗しても続けたい
# 続行できないので止める
if (-not (Test-Path $config)) { throw "設定ファイルが見つかりません: $config" }

# 1件失敗しても残りを処理したい
foreach ($f in $files) {
    if (-not (Test-Path $f)) { Write-Error "スキップ: $f"; continue }
    # ...
}

エラーを後から確認する

# 直近のエラーが新しい順に入っている
$Error[0]
$Error.Count

# その場で変数に受け取る
Get-ChildItem 'X:\a','X:\b' -ErrorAction SilentlyContinue -ErrorVariable myErr
$myErr.Count        # → 2

# PowerShell 7 の詳細表示
Get-Error
Get-Errorpwsh 7 で追加された強力なコマンドです。
直前のエラーをすべてのプロパティを展開して表示します。$Error[0] | Format-List * より読みやすく、原因の特定が速くなります。
エイリアスは gerr です(05 章で触れた「7 で追加された唯一のエイリアス」がこれです)。
PowerShell が初めての人へ
$Error自動変数で、セッション中に起きたエラーが新しい順に溜まります。$Error[0] が最新です。
対話中に「さっき何が起きたんだっけ」と思ったら $Error[0]、詳しく見たいなら Get-Error と覚えておくと便利です。

08.3 外部コマンドと終了コード

exe の失敗は例外にならない 見落としやすい

robocopygit7z などの外部コマンドは、失敗しても PowerShell のエラーになりません

try {
    cmd /c "exit 3"
    "例外なし。`$LASTEXITCODE = $LASTEXITCODE  /  `$? = $?"
} catch {
    'catch に入った'
}
# → 例外なし。$LASTEXITCODE = 3  /  $? = False
robocopy ... (失敗) try/catch では捕まらない -ErrorAction Stop も効かない $LASTEXITCODE で判定する 0 以外なら失敗(コマンドによる) PowerShell 7 なら例外にもできる $PSNativeCommandUseErrorActionPreference = $true 既定は $false。5.1 には無いので、両対応なら $LASTEXITCODE で書く
図 08.4 — 外部コマンドの失敗は自分で検知する必要がある

2つの判定方法

変数意味対象
$LASTEXITCODE直前の外部コマンドの終了コードexe のみ。Cmdlet では更新されない
$?直前のコマンドが成功したか(真偽値)Cmdlet も外部コマンドも
robocopy $src $dst /MIR
if ($LASTEXITCODE -ge 8) {
    throw "robocopy が失敗しました (終了コード: $LASTEXITCODE)"
}
robocopy の終了コードは 0 が成功とは限りません。
1 は「ファイルをコピーした」、3 は「コピーと追加削除をした」で、いずれも成功です。失敗は 8 以上とされています。
外部コマンドごとに終了コードの意味は違います。0 -ne $LASTEXITCODE で一律に失敗と判定すると、正常なのにエラー扱いになります。使う前にドキュメントを確認してください。

PowerShell 7 なら例外にできる pwsh 7

$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'

try { cmd /c "exit 5" }
catch { $_.Exception.Message }
# → Program "cmd.exe" ended with non-zero exit code: 5 (0x00000005).

既定は $false です。5.1 には存在しないので、両対応のスクリプトでは $LASTEXITCODE で書くのが確実です。

スクリプト自身の終了コード

exit 0     # 正常終了
exit 1     # 異常終了
明示しないと、最後に実行した外部コマンドの終了コードを引き継ぎます。
スクリプトの末尾でたまたま exit 3 を返すコマンドを呼んでいると、スクリプト自体が「失敗」として扱われます。タスクスケジューラや CI から呼ぶ場合、これが誤判定の原因になります。
バッチや CI から呼ばれるスクリプトは、末尾に exit 0 を明示してください。
PowerShell が初めての人へ
「終了コード」は、プログラムが終わるときに OS へ返す数値です。0 が成功、それ以外が失敗というのが Unix 由来の慣習で、Windows でも同じです。
cmd の %ERRORLEVEL% と同じものを、PowerShell では $LASTEXITCODE で読みます。

08.4 デバッグ

出力を使い分ける

コマンド既定で表示戻り値に混ざるか用途
Write-Outputする混ざる本来の出力
Write-Hostする混ざらない利用者へのメッセージ
Write-Verboseしない混ざらない推奨処理の進行ログ
Write-Debugしない混ざらない開発中の詳細
Write-Warningする混ざらない注意喚起
function Get-Report {
    [CmdletBinding()]
    param([string]$Path)

    Write-Verbose "処理開始: $Path"
    # ...
    Write-Verbose "完了"
}

Get-Report -Path .\a.txt              # 何も出ない
Get-Report -Path .\a.txt -Verbose     # Write-Verbose の内容が出る
Write-Verbose は消さずに残せるデバッグ出力です。
Write-Host でデバッグすると、本番前に消す必要があります。Write-Verbose なら普段は出ず、必要なときだけ -Verbose で有効化できるので、そのまま残しておけます。
06 章で [CmdletBinding()] を付けた理由がこれです。付けないと -Verbose が使えません。

止めて中身を見る

F9 → F5 VS Code で ブレークポイント Wait-Debugger コードに書いて その場で止める Set-PSBreakpoint 変数が変わった瞬間 に止められる Set-PSDebug 実行行を全部 表示する 止めたあとは、その場のシェルにそのまま入れる 変数の中身を見るだけでなく、Get-Member でオブジェクトの構造を調べたり、 その場でコマンドを試したりできる — これが PowerShell のデバッグの強み
図 08.5 — 停止中はデバッグコンソールが通常のシェルとして使える
# 変数が変更されたときに止める
Set-PSBreakpoint -Script .\main.ps1 -Variable count -Mode Write

# 特定のコマンドが呼ばれたときに止める
Set-PSBreakpoint -Script .\main.ps1 -Command Remove-Item

# 一覧と削除
Get-PSBreakpoint
Get-PSBreakpoint | Remove-PSBreakpoint
「変数がいつ書き換わったか分からない」ときは -Variable ... -Mode Write が効きます。
どこで値が変わったのかを追うために Write-Host を撒く必要がありません。書き換わった瞬間に止まり、その時点の呼び出し履歴が見られます

行単位のトレース

Set-PSDebug -Trace 1     # 実行された行を表示
Set-PSDebug -Trace 2     # 変数代入も表示
Set-PSDebug -Off         # 解除
PowerShell が初めての人へ
PowerShell のデバッグは、停止した時点のシェルにそのまま入れるのが最大の特徴です。IDE のウォッチ式に頼らず、$data | Get-Member$data[0] | Format-List * をその場で打てます。
05 章で扱った調べ方が、そのままデバッグ手段になります。「print を撒く」前に「一度止めて中を見る」を試してください。

この章のまとめ

症状原因と対処
try/catch で捕まらない非終了エラー。-ErrorAction Stop を付ける
外部コマンドの失敗を検知できない例外にならない。$LASTEXITCODE で判定する
エラーが出るが処理は続いてほしい-ErrorAction SilentlyContinue-ErrorVariable
どこで失敗したか分からない$_.InvocationInfo.ScriptLineNumber をログに出す
エラーの詳細が知りたいGet-Error pwsh 7$Error[0] | Format-List *
CI がスクリプトを失敗扱いする末尾に exit 0 を明示する
変数がいつ変わるか追いたいSet-PSBreakpoint -Variable x -Mode Write

覚えておく3点

  1. 既定では try/catch は効かない。Cmdlet のエラーは非終了エラーなので、-ErrorAction Stop$ErrorActionPreference = 'Stop' が必要
  2. 外部コマンドの失敗は例外にならない$LASTEXITCODE を自分で見る。終了コードの意味はコマンドごとに違う(robocopy は 8 以上が失敗)
  3. デバッグ出力は Write-Verbose。普段は出ず、-Verbose で有効化できるので消さずに残せる
次章の予告 — 09. 実務パターン集
ここまでで必要な道具が揃いました。次章ではそのまま使える実務スクリプトを扱います。ログ集計、一括リネーム、バックアップ、タスクスケジューラ連携、外部コマンド呼び出し。各パターンは 50 行以内に収め、写経できる長さにします。