PowerShellでのエラー／例外処理について

## TL;DR

PowerShellのエラーハンドリングには、`Terminating Error`と`Non-Terminating Error`とに対するものがある。
`try-catch`による処理の仕方や、`Non-Terminating Error`の起こし方、これの捕まえ方などのサンプルコードをメモっておく。

## はじめに

以前の投稿（[PowerShellで例外を捕まえられない](https://qiita.com/bounoki/items/313da6fdf08ac402e47f)）で、PowerShellを実行したときに出る例外（正確には「例外っぽいエラー出力」）を捕まえるには`ErrorAction`パラーメタを渡すか`$ErrorActionPreference`変数で制御するかすればよい的なことを書いたけど、今回はもう少し丁寧にどういうことなのか調べてみた。

## エラーの種類

`Terminating Error`と`Non-Terminating Error`がある。カンタンにいうと、
`Terminating Error`は「発生すると処理がそこで中断する」エラーで、catchできる。
`Non-Terminating Error`は「発生しても処理は継続し、エラー自体は出力される」エラーで、catchできない。

この詳細は、"Hey, Scripting Guy! Blog"の[Understanding Non-Terminating Errors in PowerShell](https://blogs.technet.microsoft.com/heyscriptingguy/2015/09/16/understanding-non-terminating-errors-in-powershell/)という記事に乗ってるので、「もっと」という方はそちらを参照されたし。
また、PowerShell Documentationの[about_try_catch_finally](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_try_catch_finally?view=powershell-5.1)にも説明があるので、スペック至上主義のかたはそちらを（`Get-Help about_try_catch_finally`でも読めるかも）。


## サンプルコードとともに

エラーを起こす関数を有する`terminate_or_not.ps1`と、これをテスト実行する`terminate_or_not.Tests.ps1`を作成し挙動を確認する。

`terminate_or_not.ps1`の`Invoke-TerminatingError`は、`Terminating Error`を起こす(いきなりthrowしている)。また、`Invoke-NonTerminatingError`は、`Non-Terminating Error`を起こす(いきなり`Write-Error`している)。さらに、`Invoke-NonTerminatingErrorEx`は`Invoke-NonTerminatingError`と同じだけど、いわゆる「高度な関数」としているのでこれらは違った扱いをすることができる。具体的には`ErrorAction`パラメータによる制御を受け付ける。
これらの関数を実行するテストコード`terminate_or_not.Tests.ps1`にて、例外ハンドリングを試みる`It`を実行してみる。

まずは、`terminate_or_not.ps1`を以下に示す。

```powershell:terminate_or_not.ps1

function Invoke-TerminatingError {
    <#
    .SYNOPSIS
    TerminatingなErrorを起こします
    #>
    Write-Debug "throwはterminating errorなので、キャッチできる"
    throw "だめぽ（throw）"
}

function Invoke-NonTerminatingError {
    <#
    .SYNOPSIS
    Non-TerminatingなErrorを起こします
    #>
    Write-Debug "Write-ErrorはNon-Terminating Errorなのでキャッチできない"
    Write-Error "だめぽ（Write-Error）"
}

function Invoke-NonTerminatingErrorEx {
    <#
    .SYNOPSIS
    Non-TerminatingなErrorを起こします
    但し、Functions AdvancedなのでErrorActionで制御可能
    #>
    [CmdletBinding()]
    Param(
    )
    Write-Debug "Write-ErrorはNon-Terminating Errorなのでキャッチできない(Function Advanced)"
    Write-Error "だめぽ（Write-Error）"
}
```

続いて、テストコードである`terminate_or_not.Tests.ps1`を示す。テストは`Pester`を使用。

```powershell:terminate_or_not.Tests.ps1

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand) -replace "\.Tests\.", "."
. (Join-Path $here $sut)

Describe "Terminating Errorと Non-Terminating Errorの比較" {

    It "Terminating Errorを吐く関数を実行するとtry-catchで捕まえられるテスト" {
        $reached = $false
        $caught = $false
        try {
            Write-Verbose "エラーによりcatchに遷移するはず"
            Invoke-TerminatingError
            $reached = $true
        }
        catch {
            $caught = $true
        }
        $reached | Should Be $false
        $caught | Should Be $true
    }

    It "Non-Terminating Errorを吐く関数を実行するとtry-catchで捕まえられないテスト" {
        $reached = $false
        $caught = $false
        try {
            Write-Verbose "エラー出力は表示されるがcatchに遷移しないはず"
            Invoke-NonTerminatingError
            $reached = $true
        }
        catch {
            $caught = $true
        }
        $reached | Should Be $true
        $caught | Should Be $false
    }

    BeforeEach {
        $VerbosePreference = "Continue"
        $DebugPreference = "Continue"
    }
}

Describe "Non-Terminating Errorを吐く普通の関数と高度な関数でErrorAction設定時の比較" {

    It "普通の関数にErrorActionを指定できないテスト" {
        $reached = $false
        $caught = $false

        try {
            Write-Verbose "ErrorActionは引数として受取不可だが、Non-Terminating Errorのためcatchできない。"
            Write-Verbose "また、テスト対象自体もNon-Terminating Errorなのでcatchできず、エラー表示される。"
            Invoke-NonTerminatingError -ErrorAction "Stop"
            $reached = $true
        }
        catch {
            $caught = $true
        }
        $reached | Should Be $true
        $caught | Should Be $false
    }

    It "高度な関数にErrorActionを指定してcatchに遷移するテスト" {
        $reached = $false
        $caught = $false

        try {
            Write-Verbose "ErrorActionでNon-Terminating Errorを停止し、catchに遷移する。"
            Invoke-NonTerminatingErrorEx -ErrorAction "Stop"
            $reached = $true
        }
        catch {
            $caught = $true
        }
        $reached | Should Be $false
        $caught | Should Be $true
    }

    BeforeEach {
        $VerbosePreference = "Continue"
        $DebugPreference = "Continue"
    }
}
```

実行結果はこんなかんじ。

```text:console

PS D:\tmp> Invoke-Pester
Executing all tests in '.'

Executing script D:\tmp\terminate_or_not.Tests.ps1

  Describing Terminating Errorと Non-Terminating Errorの比較
VERBOSE: エラーによりcatchに遷移するはず
DEBUG: throwはterminating errorなので、キャッチできる
    [+] Terminating Errorを吐く関数を実行するとtry-catchで捕まえられるテスト 63ms
VERBOSE: エラー出力は表示されるがcatchに遷移しないはず
DEBUG: Write-ErrorはNon-Terminating Errorなのでキャッチできない
Invoke-NonTerminatingError : だめぽ（Write-Error）
At D:\tmp\terminate_or_not.Tests.ps1:28 char:13
+             Invoke-NonTerminatingError
+             ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Invoke-NonTerminatingError

    [+] Non-Terminating Errorを吐く関数を実行するとtry-catchで捕まえられないテスト 22ms

  Describing Non-Terminating Errorを吐く普通の関数と高度な関数でErrorAction設定時の比較
VERBOSE: ErrorActionは引数として受取不可だが、Non-Terminating Errorのためcatchできない。
VERBOSE: また、テスト対象自体もNon-Terminating Errorなのでcatchできず、エラー表示される。
DEBUG: Write-ErrorはNon-Terminating Errorなのでキャッチできない
Invoke-NonTerminatingError : だめぽ（Write-Error）
At D:\tmp\terminate_or_not.Tests.ps1:53 char:13
+             Invoke-NonTerminatingError -ErrorAction "Stop"
+             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Invoke-NonTerminatingError
    [+] 普通の関数にErrorActionを指定できないテスト 35ms
VERBOSE: ErrorActionでNon-Terminating Errorを停止し、catchに遷移する。
DEBUG: Write-ErrorはNon-Terminating Errorなのでキャッチできない(Function Advanced)
    [+] 高度な関数にErrorActionを指定してcatchに遷移するテスト 10ms
Tests completed in 131ms
Tests Passed: 4, Failed: 0, Skipped: 0, Pending: 0, Inconclusive: 0

```

全部成功した。

説明を、と思ったけど、`It`文見てもらったほうが早い気がするので割愛。