【PowerShell】try-catchでエラー処理する方法|ErrorAction・$Error・終了/非終了エラー・finally

【PowerShell】try-catchでエラー処理する方法|ErrorAction・$Error・終了/非終了エラー・finally PowerShell

PowerShellのtry-catchは、他の言語と同じ書き方に見えます。ところが実際に使うと、エラーが起きているのにcatchに入らないという現象に必ずぶつかります。これはバグではなく、PowerShellのエラーには「終了エラー」と「非終了エラー」の2種類があり、try-catchが捕まえられるのは終了エラーだけだからです。

この区別を理解しないままtry-catchを書くと、「エラー処理を書いたのに素通りする」という事故になります。逆に、ここさえ押さえれば、エラー処理は一気に確実になります。この記事では、実機のPowerShellで挙動を確認しながら、確実に動くエラー処理の書き方を解説します。

先に結論

  • try { } catch { } finally { }が基本形ですが、非終了エラーはcatchに入りません
  • 多くのコマンドのエラーは「非終了エラー」です。捕まえるには-ErrorAction Stopを付けて終了エラーにします。
  • スクリプト全体で終了エラー扱いにするなら$ErrorActionPreference = "Stop"を使います。
  • catchの中では$_(または$PSItem)でエラー情報を取得します。$_.Exception.Messageがメッセージです。
  • 例外の種類で分けるならcatch [例外の型] { }のように型を指定します(型はコマンドや状況で異なります)。
  • 外部コマンド(.exe)の失敗はtry-catchで捕まりません。$LASTEXITCODEで判定します。

スクリプトが実行できない場合の対処はps1が実行できない原因と対処、エラーをログに残す方法はログを出力する簡易レポートもあわせて参考になります。

スポンサーリンク

try-catch-finally の基本構文

まずは基本形です。tryに通常処理、catchにエラー時の処理、finallyに成功・失敗どちらでも必ず実行したい後始末を書きます。

try-catch-basic.ps1
try {
    # エラーが起きうる処理
    $result = 1 / 0
}
catch {
    # エラー時の処理($_ にエラー情報が入る)
    Write-Host "エラー: $($_.Exception.Message)"
}
finally {
    # 成功・失敗にかかわらず必ず実行
    Write-Host "後始末を実行しました"
}

ゼロ除算のような終了エラーは、この形でそのままcatchに入ります。実機でも、1 / 0$_.Exception.MessageAttempted to divide by zero.となり捕捉できました。finallyは、エラーの有無に関係なく必ず実行されます。問題は、次に説明する「非終了エラー」です。

【最重要】try-catchで捕まらないエラーがある

PowerShellのエラーには2種類あります。この違いが、エラー処理で最もつまずくポイントです。

  • 終了エラー(Terminating Error):処理を中断させるエラー。throwや、.NETメソッドの例外、ゼロ除算などです。これはcatchで捕まえられます。
  • 非終了エラー(Non-Terminating Error):エラーを表示しつつ処理を続けるエラー。多くのコマンド(Get-ChildItemで存在しないパスを指定など)の既定の動作です。これはcatchに入りません
non-terminating-not-caught.ps1
# 存在しないパスを指定。エラーは出るが catch されない
try {
    Get-ChildItem "C:\存在しないフォルダ"
}
catch {
    # ここは実行されない!
    Write-Host "捕捉した: $($_.Exception.Message)"
}

Write-Host "catch を素通りしてここまで来る" 
「エラー処理を書いたのに素通りする」の正体

実機で確認したところ、上のコードはGet-ChildItemのエラーが画面に表示されるものの、catchブロックには入りませんでした。これが非終了エラーです。多くのコマンドのエラーはこのタイプのため、try-catchで囲んだだけでは捕まえられません。次の-ErrorAction Stopで終了エラーに変える必要があります。

-ErrorAction Stop で捕まえられるようにする

非終了エラーをcatchで捕まえるには、そのコマンドに-ErrorAction Stopを付けて、終了エラーに変換します。これがPowerShellのエラー処理で最も重要なテクニックです。

erroraction-stop.ps1
try {
    # -ErrorAction Stop で終了エラーに変える
    Get-ChildItem "C:\存在しないフォルダ" -ErrorAction Stop
}
catch {
    # 今度はここに入る
    Write-Host "捕捉した: $($_.Exception.Message)"
}

実機でも、-ErrorAction Stopを付けるとcatchに入り、例外の型はItemNotFoundExceptionとして捕捉できました。エラーを捕まえたいコマンドには、忘れずに-ErrorAction Stopを付けるのが基本です。

ErrorActionPreference でまとめて終了エラー化する

コマンドごとに-ErrorAction Stopを付けるのが大変な場合は、スクリプトの先頭で$ErrorActionPreferenceStopに設定すると、すべてのコマンドのエラーが終了エラーになり、catchで捕まえられます。

erroractionpreference.ps1
# スクリプト全体で、非終了エラーも終了エラーとして扱う
$ErrorActionPreference = "Stop"

try {
    Get-ChildItem "C:\存在しないフォルダ"   # -ErrorAction なしでも catch される
    Get-Content  "C:\存在しないファイル.txt"
}
catch {
    Write-Host "捕捉した: $($_.Exception.Message)"
}

実機でも、$ErrorActionPreference = "Stop"を設定すると、-ErrorActionを付けていないコマンドの非終了エラーもcatchに入りました。既定値はContinue(エラーを表示して処理を続ける)です。ほかに、エラーを表示せず続けるSilentlyContinueなどがあります。

設定の影響範囲に注意

$ErrorActionPreferenceは、設定した場所以降に影響します。スクリプトの先頭で設定すればスクリプト全体、関数の中で設定すればその関数内が対象です。意図しない範囲に影響しないよう、設定する場所に気をつけてください。共通部品では、関数単位で設定するか、コマンドごとに-ErrorActionを指定するほうが安全な場合もあります。

catchでエラーの中身を取り出す

catchブロックの中では、自動変数$_$PSItemでも同じ)にエラー情報が入ります。ここからメッセージや例外の種類、発生場所を取り出せます。

catch-exception-info.ps1
try {
    Get-Content "C:\存在しないファイル.txt" -ErrorAction Stop
}
catch {
    $_.Exception.Message            # エラーメッセージ
    $_.Exception.GetType().FullName # 例外の型(分岐や調査に使う)
    $_.CategoryInfo.Category        # エラーの分類
    $_.ScriptStackTrace             # どこで起きたか(呼び出し経路)
    $_.InvocationInfo.ScriptLineNumber  # 行番号
}

調査でよく使うのは$_.Exception.Message(人が読むメッセージ)と$_.Exception.GetType().FullName(例外の型名)です。型名が分かれば、次に説明する型別のcatchで処理を分けられます。実機では、ゼロ除算の例外型はSystem.Management.Automation.RuntimeExceptionでした。

例外の種類で処理を分ける(catch [型])

catchは複数書けて、それぞれに例外の型を指定できます。「ファイルが無いときはこう、権限が無いときはこう」と、原因別に処理を分けられます。

catch-by-type.ps1
try {
    Get-Content "C:\data.txt" -ErrorAction Stop
}
catch [System.Management.Automation.ItemNotFoundException] {
    Write-Host "ファイルやパスが見つかりません"
}
catch [System.UnauthorizedAccessException] {
    Write-Host "アクセス権がありません"
}
catch {
    # 上のどれにも当てはまらない、その他すべて
    Write-Host "予期しないエラー: $($_.Exception.Message)"
}
型別catchは「具体的なものを先、汎用を後」に

複数のcatchは上から順に評価され、最初に一致したものだけが実行されます。型を指定しない汎用のcatchを先頭に置くと、そこですべて捕まってしまい、後ろの型別catchに届きません。具体的な型を上、型なしの汎用を最後に並べてください。なお、例外の型はコマンドや状況によって異なります。実機で確認したところ、Get-Contentで存在しないパスを指定したときの型はSystem.Management.Automation.ItemNotFoundExceptionでした(System.IO.FileNotFoundExceptionではありません)。指定する型は、前述の$_.Exception.GetType().FullNameで実際の型を確かめてから決めてください。

throwで自分でエラーを発生させる

入力チェックなどで、自分から処理を止めたいときはthrowを使います。throwは終了エラーなので、確実にcatchへ伝わります。

throw.ps1
function Get-UserAge {
    param([int]$Age)

    if ($Age -lt 0) {
        throw "年齢が不正です: $Age"   # ここで処理を中断
    }
    return $Age
}

try {
    Get-UserAge -Age -5
}
catch {
    Write-Host "捕捉: $($_.Exception.Message)"
}

# catch の中で throw すると、エラーを上位へ再送できる(再スロー)
try {
    try { throw "元のエラー" }
    catch { throw }   # そのまま上位へ投げ直す
}
catch {
    Write-Host "上位で受けた: $($_.Exception.Message)"
}
throw と Write-Error の違い

似たものにWrite-Errorがありますが、こちらは既定では非終了エラーです。実機でも、Write-Errortry-catchで囲んでもcatchに入りませんでした。「処理を止めてcatchへ渡したい」ならthrow、「エラーを記録しつつ処理は続けたい」ならWrite-Error、と使い分けます。

外部コマンドのエラーはtry-catchで捕まらない

もう一つの大きな落とし穴が、外部コマンド(.exeやバッチなど)です。これらの失敗は終了エラーにならないため、try-catchでは捕まえられません。代わりに、終了コードを表す$LASTEXITCODEで成否を判定します。

native-command-lastexitcode.ps1
# 外部コマンドの失敗は catch されない
try {
    & cmd /c "exit 3"
}
catch {
    Write-Host "ここには入らない"
}

# 終了コードで判定する(0 が成功、それ以外は失敗)
& cmd /c "exit 3"
if ($LASTEXITCODE -ne 0) {
    Write-Host "外部コマンドが失敗しました(終了コード: $LASTEXITCODE)"
}

実機でも、cmd /c "exit 3"catchに入らず、$LASTEXITCODE3になりました。外部コマンドを呼ぶスクリプトでは、実行のたびに$LASTEXITCODEを確認するのが定石です。なお、PowerShell 7.3以降では、外部コマンドのエラーも$ErrorActionPreferenceに従わせる設定($PSNativeCommandUseErrorActionPreference)が追加されています。

$Error と -ErrorVariable でエラーを記録する

発生したエラーは、自動変数$Errorに新しい順で蓄積されます。$Error[0]が直近のエラーです。特定のコマンドのエラーだけを変数に集めたいときは-ErrorVariableを使います。

error-variable.ps1
# 直近のエラーを参照する
try { 1 / 0 } catch {}
$Error[0].Exception.Message   # Attempted to divide by zero.
$Error.Count                  # 蓄積されたエラー件数

# 特定コマンドのエラーだけを変数に集める(非終了エラーも拾える)
Get-ChildItem "C:\無い1", "C:\無い2" `
    -ErrorAction SilentlyContinue -ErrorVariable myErrors

Write-Host "エラー件数: $($myErrors.Count)" 

実機でも、$Error[0]に直近のゼロ除算メッセージが入り、-ErrorVariableでエラーを変数に集められました。-ErrorVariableは、エラーを止めずに記録だけしたい一括処理(複数ファイルの処理など)で役立ちます。エラーをファイルに残すなら、ログ出力の方法と組み合わせます。バッチ処理を止めずにエラーを集計する考え方はプロセスを監視して自動再起動する方法でも触れています。

よくある失敗

try-catchで囲んだのにエラーが素通りする

多くのコマンドのエラーは非終了エラーで、catchに入りません。捕まえたいコマンドには-ErrorAction Stopを付けるか、$ErrorActionPreference = "Stop"を設定します。

汎用catchを先頭に書いて型別catchが効かない

catchは上から順に評価されます。型なしの汎用catchを先頭に置くと、そこで全部捕まります。具体的な型を上、汎用を最後に並べてください。

外部コマンドの失敗をtry-catchで捕まえようとする

外部コマンドの失敗は終了エラーになりません。$LASTEXITCODEで終了コードを確認して判定します。

throwのつもりでWrite-Errorを使い処理が止まらない

Write-Errorは既定で非終了エラーのため、処理は続きます。確実に止めてcatchへ渡したいならthrowを使います。

ErrorActionPreferenceの設定範囲を意識しない

$ErrorActionPreference = "Stop"は設定以降に影響します。共通関数の中で安易に設定すると、呼び出し元の挙動まで変えてしまうことがあるため、範囲を意識してください。

よくある質問

Qなぜtry-catchでエラーが捕まらないのですか?
Aそのエラーが「非終了エラー」だからです。PowerShellの多くのコマンドは、既定でエラーを表示しつつ処理を続ける非終了エラーを出します。catchで捕まえるには-ErrorAction Stopを付けて終了エラーに変えてください。
Q-ErrorAction Stop と $ErrorActionPreference のどちらを使うべきですか?
A特定のコマンドだけ確実に捕まえたいなら-ErrorAction Stop、スクリプト全体をまとめて終了エラー扱いにしたいなら$ErrorActionPreference = "Stop"が便利です。共通部品では影響範囲を考え、コマンド単位の指定を選ぶ場面もあります。
Qcatchで例外の種類を知るには?
A$_.Exception.GetType().FullNameで型名が分かります。これをcatch [型名]に指定すれば、原因別に処理を分けられます。メッセージは$_.Exception.Messageです。
Qfinallyは必ず実行されますか?
Aはい。tryが成功しても、catchでエラーを処理しても、finallyは必ず実行されます。ファイルや接続のクローズなど、後始末の処理を書くのに向いています。
Q外部コマンドの成否はどう判定しますか?
A$LASTEXITCODEを確認します。0が成功、それ以外は失敗です。外部コマンドの失敗はtry-catchでは捕まらないため、呼び出すたびに終了コードをチェックしてください。

まとめ

  • try { } catch { } finally { }が基本形ですが、非終了エラーはcatchに入りません
  • 捕まえたいコマンドには-ErrorAction Stop、全体なら$ErrorActionPreference = "Stop"を使います。
  • catchでは$_.Exception.MessageGetType().FullNameでエラー情報を取得します。
  • 原因別に分けるならcatch [型]を、具体的な型を上・汎用を最後に並べます。
  • 自分で止めるならthrowWrite-Errorは非終了なので処理は続きます。
  • 外部コマンドの失敗は$LASTEXITCODEで判定します。

PowerShellのエラー処理でつまずく原因は、ほぼすべて「終了エラーと非終了エラーの区別」に集約されます。捕まえたいエラーは-ErrorAction Stopで終了エラーに変える、という一点を押さえれば、try-catchは他の言語と同じように、狙ったとおりに動いてくれます。