次の方法で共有


例外について知りたいこと

エラー処理は、コードの記述に関しては、人生の一部にすぎません。 多くの場合、予期される動作の条件を確認して検証できます。 予期しない状況が発生した場合は、例外処理に変わります。 他のユーザーのコードによって生成された例外を簡単に処理することも、他のユーザーが処理できるように独自の例外を生成することもできます。

この記事の 元のバージョン は、 @KevinMarquetteによって書かれたブログに掲載されました。 PowerShell チームは、このコンテンツを Microsoft と共有してくれた Kevin に感謝します。 PowerShellExplained.com で彼のブログをチェックしてください。

基本的な用語

この用語に進む前に、いくつかの基本的な用語について説明する必要があります。

例外

例外は、通常のエラー処理で問題に対処できない場合に作成されるイベントに似ています。 数値を 0 で除算しようとしたり、メモリ不足にしたりする場合は、例外が発生する例があります。 使用しているコードの作成者が、特定の問題が発生したときに例外を作成することがあります。

投げるとキャッチする

例外が発生すると、例外がスローされると言います。 スローされた例外を処理するには、それをキャッチする必要があります。 例外がスローされ、何かによってキャッチされない場合、スクリプトは実行を停止します。

呼び出し履歴

呼び出し履歴は、相互に呼び出された関数の一覧です。 関数が呼び出されると、スタックまたはリストの先頭に追加されます。 関数が終了または返されると、スタックから削除されます。

例外がスローされると、その呼び出し履歴は、例外ハンドラーがそれをキャッチするためにチェックされます。

終了エラーと終了しないエラー

例外は通常、終了エラーです。 スローされた例外はキャッチされるか、現在の実行を終了させるかのいずれかです。 既定では、 Write-Error によって終了しないエラーが生成され、例外をスローせずに出力ストリームにエラーが追加されます。

私はこれを指摘するのは、 Write-Error やその他の終了しないエラーが catchをトリガーしないためです。

例外を飲み込む

エラーを抑制するためだけに、このエラーをキャッチする場合です。 トラブルシューティングの問題が非常に困難になる可能性があるため、注意してこれを行います。

基本的なコマンド構文

PowerShell で使用される基本的な例外処理構文の概要を次に示します。

投げる

独自の例外イベントを作成するには、 throw キーワードを使用して例外をスローします。

function Start-Something
{
    throw "Bad thing happened"
}

これにより、終了エラーであるランタイム例外が作成されます。 呼び出し元関数の catch によって処理されるか、次のようなメッセージでスクリプトを終了します。

PS> Start-Something

Bad thing happened
At line:1 char:1
+ throw "Bad thing happened"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (Bad thing happened:String) [], RuntimeException
    + FullyQualifiedErrorId : Bad thing happened

Write-Error -ErrorAction 停止

私は、 Write-Error がデフォルトで終了エラーをスローしないことを言及しました。 -ErrorAction Stopを指定すると、Write-Errorは、catchで処理できる終了エラーを生成します。

Write-Error -Message "Houston, we have a problem." -ErrorAction Stop

Lee Dailey さん、このように -ErrorAction Stop を使用することを思い出させていただき、ありがとうございます。

コマンドレットの -ErrorAction 停止

任意の高度な関数またはコマンドレットに -ErrorAction Stop を指定すると、すべての Write-Error ステートメントが、実行を停止する、または catchで処理できる終了エラーに変換されます。

Start-Something -ErrorAction Stop

ErrorAction パラメーターの詳細については、「about_CommonParameters」を参照してください。 $ErrorActionPreference 変数の詳細については、about_Preference_Variablesを参照してください。

Try/Catch

PowerShell(および他の多くの言語)での例外処理の仕組みは、最初にコードの一部をtryし、エラーが発生した場合にcatchするというものです。 簡単なサンプルを次に示します。

try
{
    Start-Something
}
catch
{
    Write-Output "Something threw an exception"
    Write-Output $_
}

try
{
    Start-Something -ErrorAction Stop
}
catch
{
    Write-Output "Something threw an exception or used Write-Error"
    Write-Output $_
}

catch スクリプトは、終了エラーが発生した場合にのみ実行されます。 tryが正しく実行されると、catchをスキップします。 catch ブロック内の例外情報には、$_変数を使用してアクセスできます。

トライ/ファイナリー

場合によっては、エラーを処理する必要はありませんが、例外が発生した場合に実行するコードが必要な場合があります。 finallyスクリプトはまさにそうします。

この例を見てみましょう。

$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()

リソースを開いたり、リソースに接続したりするたびに、リソースを閉じる必要があります。 ExecuteNonQuery()が例外をスローした場合、接続は閉じられません。 try/finally ブロック内の同じコードを次に示します。

$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
try
{
    $command.Connection.Open()
    $command.ExecuteNonQuery()
}
finally
{
    $command.Connection.Close()
}

この例では、エラーが発生した場合、接続は閉じられます。 また、エラーがない場合は閉じられます。 finally スクリプトは毎回実行されます。

例外をキャッチしていないため、引き続き呼び出し履歴に反映されます。

Try/Catch/Finally

catchfinallyを一緒に使用することは完全に有効です。 ほとんどの場合、いずれかを使用しますが、両方を使用するシナリオが見つかる場合があります。

$PSItem

これで基本的なことを終えたので、もう少し深く踏み込むことができます。

catch ブロック内には、例外に関する詳細を含む$PSItem型の自動変数 ($_またはErrorRecord) があります。 いくつかの主要なプロパティの概要を次に示します。

これらの例では、 ReadAllText で無効なパスを使用してこの例外を生成しました。

[System.IO.File]::ReadAllText( '\\test\no\filefound.log')

PSItem.ToString()

これにより、ログ記録と一般的な出力で使用する最もクリーンなメッセージが提供されます。 ToString() は、 $PSItem が文字列内に配置された場合に自動的に呼び出されます。

catch
{
    Write-Output "Ran into an issue: $($PSItem.ToString())"
}

catch
{
    Write-Output "Ran into an issue: $PSItem"
}

$PSItem.InvocationInfo

このプロパティは、例外がスローされた関数またはスクリプトについて、PowerShell によって収集された追加情報を含みます。 作成したサンプル例外の InvocationInfo を次に示します。

PS> $PSItem.InvocationInfo | Format-List *

MyCommand             : Get-Resource
BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 5
OffsetInLine          : 5
ScriptName            : C:\blog\throwerror.ps1
Line                  :     Get-Resource
PositionMessage       : At C:\blog\throwerror.ps1:5 char:5
                        +     Get-Resource
                        +     ~~~~~~~~~~~~
PSScriptRoot          : C:\blog
PSCommandPath         : C:\blog\throwerror.ps1
InvocationName        : Get-Resource

ここでの重要な詳細は、 ScriptName、コードの Line 、および呼び出しが開始された ScriptLineNumber を示しています。

$PSItem.ScriptStackTrace

このプロパティは、例外が生成されたコードを取得した関数呼び出しの順序を示します。

PS> $PSItem.ScriptStackTrace
at Get-Resource, C:\blog\throwerror.ps1: line 13
at Start-Something, C:\blog\throwerror.ps1: line 5
at <ScriptBlock>, C:\blog\throwerror.ps1: line 18

私は同じスクリプト内の関数を呼び出すだけですが、複数のスクリプトが関係していた場合、これは呼び出しを追跡します。

$PSItem.Exception

これが本当に投げられた例外です。

$PSItem.Exception.Message

これは例外を説明する一般的なメッセージであり、トラブルシューティングの出発点として適しています。 ほとんどの例外には既定のメッセージがありますが、例外が発生した際に独自のメッセージに設定することも可能です。

PS> $PSItem.Exception.Message

Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."

これもErrorRecordに設定されていない場合に$PSItem.ToString()を呼び出すと返されるメッセージです。

$PSItem.Exception.InnerException

例外には内部例外を含めることができます。 これは、多くの場合、呼び出しているコードが例外をキャッチし、別の例外をスローする場合です。 元の例外は、新しい例外内に配置されます。

PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.

例外の再スローについて説明する際に、後でこの話題に戻ります。

$PSItem.Exception.StackTrace

これは例外の StackTrace です。 上記の ScriptStackTrace を示しましたが、これはマネージド コードの呼び出し用です。

at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean
 useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs,
 String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32
 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean
 checkHost)
at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks,
 Int32 bufferSize, Boolean checkHost)
at System.IO.File.InternalReadAllText(String path, Encoding encoding, Boolean checkHost)
at CallSite.Target(Closure , CallSite , Type , String )

マネージド コードからイベントが発生した場合にのみ、このスタック トレースが取得されます。 この例では、.NET Framework 関数を直接呼び出していることしかわかりません。 一般に、スタック トレースを見ているときは、コードが停止し、システム呼び出しが開始される場所を探します。

例外の処理

基本的な構文と例外プロパティ以外にも例外があります。

型指定例外の捕捉

キャッチする例外を選択できます。 例外には型があり、キャッチする例外の種類を指定できます。

try
{
    Start-Something -Path $path
}
catch [System.IO.FileNotFoundException]
{
    Write-Output "Could not find $path"
}
catch [System.IO.IOException]
{
        Write-Output "IO error with the file: $path"
}

例外の種類は、例外と一致する catch ブロックが見つかるまで、各ブロックでチェックされます。 例外は他の例外から継承できることを認識することが重要です。 上記の例では、 FileNotFoundExceptionIOExceptionから継承しています。 したがって、 IOException が最初の場合は、代わりに呼び出されます。 複数の一致がある場合でも、呼び出される catch ブロックは 1 つだけです。

System.IO.PathTooLongExceptionがある場合、IOExceptionは一致しますが、InsufficientMemoryExceptionがある場合は何もキャッチされません。スタックに伝達されます。

一度に複数の型をキャッチする

同じ catch ステートメントで複数の例外の種類をキャッチできます。

try
{
    Start-Something -Path $path -ErrorAction Stop
}
catch [System.IO.DirectoryNotFoundException],[System.IO.FileNotFoundException]
{
    Write-Output "The path or file was not found: [$path]"
}
catch [System.IO.IOException]
{
    Write-Output "IO error with the file: [$path]"
}

この追加を提案していただき、Redditor u/Sheppard_Ra ありがとうございます。

型指定された例外のスロー

PowerShell では、型指定された例外をスローできます。 文字列を使用して throw を呼び出す代わりに、

throw "Could not find: $path"

次のような例外アクセラレータを使用します。

throw [System.IO.FileNotFoundException] "Could not find: $path"

ただし、その場合はメッセージを指定する必要があります。

スローされる例外の新しいインスタンスを作成することもできます。 この場合、システムにはすべての組み込み例外に対する既定のメッセージがあるため、このメッセージは省略可能です。

throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")

PowerShell 5.0 以降を使用していない場合は、以前の New-Object アプローチを使用する必要があります。

throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")

型指定された例外を使用すると、前のセクションで説明したように、ユーザー (または他のユーザー) が型によって例外をキャッチできます。

Write-Error -Exception

これらの型指定された例外を Write-Error に追加できますが、例外の種類によってエラーを catch することもできます。 次の例のような Write-Error を使用します。

# with normal message
Write-Error -Message "Could not find path: $path" -Exception ([System.IO.FileNotFoundException]::new()) -ErrorAction Stop

# With message inside new exception
Write-Error -Exception ([System.IO.FileNotFoundException]::new("Could not find path: $path")) -ErrorAction Stop

# Pre PS 5.0
Write-Error -Exception ([System.IO.FileNotFoundException]"Could not find path: $path") -ErrorAction Stop

Write-Error -Message "Could not find path: $path" -Exception (New-Object -TypeName System.IO.FileNotFoundException) -ErrorAction Stop

このようにキャッチすることができます。

catch [System.IO.FileNotFoundException]
{
    Write-Log $PSItem.ToString()
}

.NET 例外の大きな一覧

この記事を補完するために、何百もの .NET 例外を含む Reddit r/PowerShell コミュニティの助けを借りてマスター リストをコンパイルしました。

まず、そのリストを検索して、自分の状況に適していると思われる例外を探します。 基本 System 名前空間で例外を使用する必要があります。

例外はオブジェクトです

型指定された例外の多くを使用し始めた場合は、それらがオブジェクトであることを覚えておいてください。 例外が異なると、コンストラクターとプロパティが異なります。 System.IO.FileNotFoundException ドキュメントを見ると、メッセージとファイル パスを渡すことができることがわかります。

[System.IO.FileNotFoundException]::new("Could not find file", $path)

また、そのファイル パスを公開する FileName プロパティがあります。

catch [System.IO.FileNotFoundException]
{
    Write-Output $PSItem.Exception.FileName
}

他のコンストラクターとオブジェクトのプロパティについては 、.NET のドキュメント を参照してください。

例外を再スローする

catch ブロックで行う作業がすべて同じ例外throw場合は、catchしないでください。 例外が発生したときに処理または実行する予定の例外のみを catch する必要があります。

例外に対してアクションを実行し、ダウンストリームで処理できるように例外を再スローする場合があります。 メッセージを書いたり、問題を検出した場所の近くにログを記録したりできますが、問題をさらにスタックの上に処理することもできます。

catch
{
    Write-Log $PSItem.ToString()
    throw $PSItem
}

興味深いことに、catchの内部からthrowを呼び出すと、現在の例外が再スローされます。

catch
{
    Write-Log $PSItem.ToString()
    throw
}

ソース スクリプトや行番号などの元の実行情報を保持するために、例外を再スローする必要があります。 この時点で新しい例外をスローすると、例外が開始された場所が非表示になります。

新しい例外を再スローする

例外をキャッチした場合でも、別の例外をスローしたいときは、元の例外を新しい例外でラップする必要があります。 これにより、スタックの下にいるユーザーは、 $PSItem.Exception.InnerExceptionとしてアクセスできます。

catch
{
    throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}

$PSCmdlet.ThrowTerminatingError()

生の例外に throw を使用するのが気に入らないのは、エラー メッセージが throw ステートメントを指し示し、その行が問題の場所であることを示していることです。

Unable to find the specified file.
At line:31 char:9
+         throw [System.IO.FileNotFoundException]::new()
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], FileNotFoundException
    + FullyQualifiedErrorId : Unable to find the specified file.

31行目に throw を呼び出したため、スクリプトが壊れているというエラー メッセージが表示されるのは、スクリプトのユーザーにとって不適切なメッセージです。 それらに何も役に立つことを伝えません。

デクスター・ダミは、私は ThrowTerminatingError() を使用して修正できることを指摘しました。

$PSCmdlet.ThrowTerminatingError(
    [System.Management.Automation.ErrorRecord]::new(
        ([System.IO.FileNotFoundException]"Could not find $Path"),
        'My.ID',
        [System.Management.Automation.ErrorCategory]::OpenError,
        $MyObject
    )
)

ThrowTerminatingError()という関数内でGet-Resourceが呼び出されたと仮定すると、これがエラーになります。

Get-Resource : Could not find C:\Program Files (x86)\Reference
Assemblies\Microsoft\Framework\.NETPortable\v4.6\System.IO.xml
At line:6 char:5
+     Get-Resource -Path $Path
+     ~~~~~~~~~~~~
    + CategoryInfo          : OpenError: (:) [Get-Resource], FileNotFoundException
    + FullyQualifiedErrorId : My.ID,Get-Resource

問題の原因として Get-Resource 関数を指すしくみはありますか。 これは、ユーザーに役立つ情報を示します。

$PSItemErrorRecordであるため、このやり方ThrowTerminatingErrorで再スローすることもできます。

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

これにより、エラーの原因がコマンドレットに変更され、コマンドレットのユーザーから関数の内部が非表示になります。

Try は終了エラーを作成できます

Kirk Munro 氏は、一部の例外は、 try/catch ブロック内で実行された場合にのみエラーを終了していると指摘しています。 彼が私に教えてくれたゼロでの除算によるランタイム例外を発生させる例がこちらです。

function Start-Something { 1/(1-1) }

この手順に従い実行するとエラーを発生させ、その際メッセージが出力されるのが確認できます。

&{ Start-Something; Write-Output "We did it. Send Email" }

しかし、同じコードを try/catch内に配置することで、他のことが起こることがわかります。

try
{
    &{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
    Write-Output "Notify Admin to fix error and send email"
}

エラーは終了エラーになり、最初のメッセージは出力されません。 私がこの問題について気に入らないのは、このコードを関数に含めることができるということです。誰かが try/catchを使用している場合は動作が異なります。

私は自分自身でこの問題に遭遇していないが、それは注意すべき限界事例です。

$PSCmdlet.ThrowTerminatingError() 内の try/catch

$PSCmdlet.ThrowTerminatingError()の違いの 1 つは、コマンドレット内に終了エラーが発生しますが、コマンドレットを終了した後に終了しないエラーに変わるということです。 これにより、関数の呼び出し元がエラーの処理方法を決定する負担が残ります。 -ErrorAction Stopを使用するか、try{...}catch{...}内から呼び出すことで、終了エラーに戻すことができます。

パブリック関数テンプレート

私がカーク・ムンロとの会話で得た最後のポイントは、彼が彼のすべての高度な関数において、すべてのtry{...}catch{...}beginprocessブロックの周りにendを配置するということでした。 これらの汎用 catch ブロックでは、関数から発生するすべての例外に対処するために、$PSCmdlet.ThrowTerminatingError($PSItem) を使用する1行のコードがあります。

function Start-Something
{
    [CmdletBinding()]
    param()

    process
    {
        try
        {
            ...
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
}

すべてが関数内の try ステートメント内にあるため、すべてが一貫して機能します。 これにより、生成されたエラーから内部コードを非表示にするクリーン エラーもエンド ユーザーに提供されます。

トラップ

私は例外の try/catch 側面に焦点を当てた。 ただし、これをまとめる前に説明する必要があるレガシ機能が 1 つあります。

trapは、そのスコープ内で発生するすべての例外をキャッチするスクリプトまたは関数に配置されます。 例外が発生すると、 trap 内のコードが実行され、通常のコードが続行されます。 複数の例外が発生した場合、トラップは何度も呼び出されます。

trap
{
    Write-Log $PSItem.ToString()
}

throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')

私は個人的にこのアプローチを採用したことはありませんが、管理者またはコントローラースクリプトで、すべての例外をログに記録し、引き続き実行する値を見ることができます。

締めの言葉

スクリプトに適切な例外処理を追加すると、スクリプトの安定性が向上するだけでなく、これらの例外のトラブルシューティングも簡単になります。

私は例外処理について話すときの核となる概念であるため、 throw を話すのに多くの時間を費やしました。 PowerShell には、Write-Errorを使用するすべての状況を処理するthrowも用意されています。 そのため、これを読んだ後に throw を使用する必要はないと思います。

この詳細で例外処理について書く時間を取ったので、コードでエラーを生成するために Write-Error -Stop の使用に切り替えます。 私はまた、カークのアドバイスを受けて、ThrowTerminatingError をすべての関数のgoto例外ハンドラにするつもりです。