PowerShell のエラーハンドリングを（今度こそ）理解する

# PowerShell のエラーハンドリングを（今度こそ）理解する

PowerShell はエラー処理がとても難しい言語として知られています。
きちんと理解しようとすると途端に深みにハマってしまいます。
それこれもエラーの挙動が正しくドキュメント化されていないことが原因なのですが。

そこで、今度こそ PowerShell のエラーをきちんと理解しようといろいろ調べました。
結果としてまだまだ腑に落ちない箇所がありつつも、一定程度は理解できたような気がしましたのでここにまとめておきたいと思います。

## おことわり

* 本記事は PowerShell の中の人である **Michael Klement** 氏が[こちらのGitHub Issue](https://github.com/MicrosoftDocs/PowerShell-Docs/issues/1583)にコメントしている内容を基に、**筆者の独自検証の結果やそれに基づく解釈を加えて**います

* PowerShell の実際の挙動を把握することに重点を置いているため、公式ドキュメントに記載のない内容や独自の用語を記載・使用しています

* **内容の正確性は一切保証**しません。間違いがありましたらやんわりとご指摘いただければ幸いです


## 検証環境
* 以下バージョンのPowerShellで挙動を確認しています
  特に PowerShell 7 はまだプレビューなので今後動作が変わる可能性があります
  * Windows PowerShell 5.1
  * PowerShell Core 6.2.3
  * PowerShell 7.0.0-preview.5

* Windows 10 バージョン1903のみで挙動を確認しています

## エラーの種類
PowerShellのエラーには

* **終了しないエラー** (Non-terminating Errors) 
* **ステートメント終了エラー** (Statement-terminating Errors)
* **スクリプト終了エラー** (Script-terminating Errors) 

の3種類のエラーがあります。

> PowerShell のドキュメントには 終了しないエラー(Non-terminating Errors) と 終了するエラー(Terminating Errors) の2種類しか記載がありませんが、その2種類では PowerShell の実際の挙動を説明するのに十分ではないため、ここでは 終了するエラー をさらに スクリプト終了エラー と ステートメント終了エラー に細分化して説明していきます

> より厳密に"エラー"を分類するならここに挙げる3種類のエラーの他にもう一つ **構文解析エラー** (Parsing Errors) を追加すべきかもしれません。これはPowerShellコマンドの構文に致命的な問題がある場合に発生するエラーで、PowerShellスクリプトが**実行される前に発生**します。このエラーをスクリプト内でハンドリングする方法は存在しません。そのためこの記事内では扱わないことにしました。

#### 終了しないエラー (Non-terminating Errors)

* このエラーが発生した場合でも、スクリプトは**停止せずその後の処理は続行**されます

* 発生時にはコンソールにエラーメッセージが出力されるとともに、エラー情報が`$Error`自動変数に格納されます
  これはスクリプト終了エラーおよびステートメント終了エラーの場合も同様です

* PowerShellのコマンドレットが出力するエラーのほとんどが**終了しないエラー**です  

```PowerShell
Get-Item -Path "存在しないパス"  #ここでエラーが発生するが、次の行も実行される
Write-Output "処理終了"
```

#### ステートメント終了エラー (Statement-terminating Errors)

* PowerShellのドキュメント上では **終了するエラー** (Terminating Errors) として扱われます

* このエラーが発生した場合、実行中のステートメントが終了しますが**呼び出し元のスクリプトは継続実行されます**
    
* PowerShellのランタイムエラー や .NETの例外 (Exceptions) はステートメント終了エラーです
  * ランタイムエラーの例： `1 / 0`
  * .NET例外の例： `[int]::Parse("hoge")`

```PowerShell
[int]::Parse("hoge")    #.NET例外発生
Write-Output "処理終了"  #この行は実行される
```

#### スクリプト終了エラー (Script-terminating Errors)

* PowerShellのドキュメント上では **終了するエラー** (Terminating Errors) として扱われます

* このエラーが発生した時点で、**その呼び出し元も含め**、現在実行中のスクリプト全体が**即座に終了**します  
    
* スクリプト終了エラーを発生させる唯一の方法は`throw`キーワードを使うことです

```PowerShell
throw
Write-Output "処理終了"  #この行は実行されない
```

### サンプルスクリプト

3つのエラーの違いが分かるよう、もう少し詳しい例を示します。

- **終了しないエラー**

```PowerShell
function Invoke-NonTerminatingError {
  [CmdletBinding()] param()
  Write-Output "関数開始"
  Get-Item -Path "存在しないパス" # 終了しないエラー発生
  Write-Output "関数終了"
}

Write-Output "スクリプト開始"
Invoke-NonTerminatingError
Write-Output "スクリプト終了"
```
```
PS C:> .\test.ps1
スクリプト開始
関数開始
ObjectNotFound: (C:\test\存在しないパス:String) [Get-Item], ItemNotFoundException
関数終了
スクリプト終了
```

関数の実行中にエラーが発生しましたが、**関数内の後続処理も呼び出し元の処理も継続**していることが分かります。


- **ステートメント終了エラー**

```PowerShell
function Invoke-StatementTerminatingError {
  [CmdletBinding()] param()
  Write-Output "関数開始"
  # ステートメント終了エラー発生
  $PSCmdlet.ThrowTerminatingError(
    [System.Management.Automation.ErrorRecord]::new(
      'exception message',
      'errorId',
      [System.Management.Automation.ErrorCategory]::InvalidResult,
      $null
    )
  )
  Write-Output "関数終了"
}

Write-Output "スクリプト開始"
Invoke-StatementTerminatingError
Write-Output "スクリプト終了"
```
```
PS C:> .\test.ps1
スクリプト開始
関数開始
InvalidResult: (:) [Invoke-StatementTerminatingError], Exception
スクリプト終了
```

ステートメント終了エラーを意図的に発生させるために`$PSCmdlet.ThrowTerminatingError()`というメソッドを使用しています（詳細は後述）
ステートメント終了エラーが発生した時点で関数`Invoke-StatementTerminatingError`が終了したため、`Write-Output "関数終了"`は実行されていません。しかし、**呼び出し元の処理は継続**されるため`Write-Output "スクリプト終了"`は実行されています。


- **スクリプト終了エラー**

```PowerShell
function Invoke-ScriptTerminatingError {
  [CmdletBinding()] param()
  Write-Output "関数開始"
  throw # スクリプト終了エラー発生
  Write-Output "関数終了"
}

Write-Output "スクリプト開始"
Invoke-ScriptTerminatingError
Write-Output "スクリプト終了"
```
```
PS C:> .\test.ps1
スクリプト開始
関数開始
OperationStopped: (:) [], Exception
```
`throw`を実行した時点ですべての処理が終了し、関数も呼び出し元も**後続の処理は実行されていません。**


## エラー発生をトラップする方法

### `try`ステートメント
* **ステートメント終了エラー**および**スクリプト終了エラー**は`try`ステートメントを使用してトラップできます。  

* **終了しないエラーは`try`でトラップできません。**
  後述する`$ErrorActionPreference`変数や`-ErrorAction`共通パラメータを使用したエラー挙動変更と併用することで、`try`でトラップが可能になります。

```PowerShell
try {
  #ステートメント終了エラーもしくはスクリプト終了エラーが発生する可能性がある処理
  Write-Output 'Start'
  throw 'Error!'  #スクリプト終了エラー
  Write-Output 'End'  #実行されない
}
catch {
  #エラー発生時の処理
  Write-Output 'Catch!'
  Write-Output ('Error message is ' + $_.Exception.Message)
}
```

* `try`ブロック内でエラーが発生した時点で`catch`ブロックに処理が移行します  

* `try`ブロック内の**エラー発生後の後続処理は実行されません。**  

* コンソールへの**エラーメッセージ出力もされません**が、`$Error`自動変数にはエラー情報が格納されます

* 発生したエラーを含むエラーレコードは **`catch`ブロック内に限り`$_`自動変数に格納**されています

* `catch`ブロック内で`throw`ステートメントを引数なしで実行すると、`try`ブロック内で発生したエラーを再スローできます
  この場合、発生したエラーの種類に関わらず**必ずスクリプト終了エラー**として再スローされます


`catch`対象の例外型を指定したり、エラー発生有無に関わらず必ず実行される`finally`ブロックを使用する例も載せておきます。

```PowerShell
try {
    Non-ExsitenceCommand
}
catch [System.Management.Automation.CommandNotFoundException] {
    Write-Output "そんなコマンドは存在しない！"
}
finally {
  Write-Output "終了"
}
```

エラーをトラップする必要はないけど、エラー発生後にも実行したい処理がある場合、`catch`ブロックのない`try-finally`という書き方も利用可能です。

```PowerShell
try {
  # エラーが発生する可能性がある処理...
}
finally {
  Write-Output "これは必ず実行される"
}
```

### `trap`ステートメント
`trap`ステートメントは`try-catch`に比べて**扱いづらいので利用する機会は少ない**かと思いますが、挙動を正しく理解して使うと便利なことが（ごく稀に）あります。

* **ステートメント終了エラー**および**スクリプト終了エラー**は`trap`ステートメントを使用してトラップできます。

* **終了しないエラーは`trap`でトラップできません。**
  後述する`$ErrorActionPreference`変数や`-ErrorAction`共通パラメータを使用したエラー挙動変更と併用することで、`trap`でトラップが可能になります。

```PowerShell
trap {
  # エラー発生時の処理
  Write-Output 'Error Occurred.'
}

Write-Output 'Start'
throw #スクリプト終了エラー
Write-Output 'End'
```
```
Start
Error Occurred.
ScriptHalted
発生場所 F:\ErrorHandlingPS\test.ps1:7 文字:1
+ throw #スクリプト終了エラー
+ ~~~~~
    + CategoryInfo          : OperationStopped: (:) [], RuntimeException
    + FullyQualifiedErrorId : ScriptHalted
 
End
```

* `trap`ステートメントと**同じか内包されたスコープで**エラーが発生した際に、`trap`ブロック内の処理が実行されます

* `trap`は**同じスコープ内であれば**どこに書いても構いません
  スクリプトの最後などエラー発生行より後に書いても正しくトラップされます

* `try-catch`とは異なり、**コンソールにはエラーメッセージが出力され、`trap`ブロックと同じスコープにあるエラー発生箇所以後の処理から継続実行されます**  

#### `trap`ステートメントとスコープの関係性
`trap`ステートメントはスクリプト内のどこに書くか（どのスコープに属するか）を意識していないと予期せぬ挙動になることがあります。

```PowerShell
function function1 {
  trap { Write-Output 'Error Occurred.' }
  throw #スクリプト終了エラー
  Write-Output "function1 終了" #実行される
}

function1
Write-Output "スクリプト終了"
```
```
Error Occurred.
ScriptHalted
発生場所 F:\ErrorHandlingPS\test.ps1:3 文字:5
+     throw #スクリプト終了エラー
+     ~~~~~
    + CategoryInfo          : OperationStopped: (:) [], RuntimeException
    + FullyQualifiedErrorId : ScriptHalted
 
function1 終了
スクリプト終了
```

上記例の場合、`trap`は`function1`内のスコープに属しているため`throw`でスクリプト終了エラーが発生した後、`trap`内の処理が実行され、**処理は`throw`の次の行に戻ります**。よって`Write-Output "function1 終了"`は**実行されます**。

```PowerShell
function function2 {
  throw #スクリプト終了エラー
  Write-Output "function2 終了" #実行されない
}

trap { Write-Output 'Error Occurred.' }
function2
Write-Output "スクリプト終了"
```
```
Error Occurred.
ScriptHalted
発生場所 F:\ErrorHandlingPS\test.ps1:2 文字:5
+     throw #スクリプト終了エラー
+     ~~~~~
    + CategoryInfo          : OperationStopped: (:) [], RuntimeException
    + FullyQualifiedErrorId : ScriptHalted
 
スクリプト終了
```

上記例では、`trap`はスクリプト全体のスコープに属しています。
`function2`のスコープはスクリプト全体スコープに内包されているため、`function2`内のエラーもトラップされて`trap`内の処理が実行されますが、**処理は`trap`と同じスクリプト全体スコープの次の行である**`Write-Output "スクリプト終了"`に戻ります。
よって`Write-Output "function2 終了"`は**実行されません**。

#### `continue`、`break`キーワード
`trap`ブロック内では`continue`と`break`キーワードを使用して**エラートラップ後の挙動を変更**できます。

```PowerShell
trap {
  Write-Output 'Error Occurred.'
  continue
}

Write-Output 'Start'
throw #スクリプト終了エラー
Write-Output 'End'  #trap後に実行される
```
```
Start
Error Occurred.
End
```

`trap`ブロック内で`continue`が現れると、`trap`内の処理は**そこで終了**し`trap`後の処理が続行されます。
また、**コンソールにエラーメッセージが出力されなくなります。**

```PowerShell
trap {
  Write-Output 'Error Occurred.'
  break
}

Write-Output 'Start'
throw #スクリプト終了エラー
Write-Output 'End'  #実行されない
```
```
Start
Error Occurred.
ScriptHalted
発生場所 F:\ErrorHandlingPS\test.ps1:7 文字:3
+   throw #スクリプト終了エラー
+   ~~~~~
    + CategoryInfo          : OperationStopped: (:) [], RuntimeException
    + FullyQualifiedErrorId : ScriptHalted
```

`trap`ブロック内で`break`が現れると、エラーの種類や後続処理の有無に関わらず**そこでスクリプトが終了します**。
`trap`が関数内のスコープに属している場合でも、呼び出し元も含めてスクリプト全体が終了することに注意してください。

## エラー発生時の挙動を変更する方法
### `$ErrorActionPreference`自動変数
`$ErrorActionPreference`自動変数の 値を変更することでスクリプト単位でエラー発生時の挙動を変更することができます。
PowerShellではデフォルトで`Continue`が指定されています。

* `Continue` - デフォルトの挙動

* `Stop` - すべてのエラーの挙動が **スクリプト終了エラー に変更されます**

* `SilentlyContinue`
  * **終了しないエラー および ステートメント終了エラー** の場合  
    エラーとしての挙動は変化しませんが、コンソールにエラーメッセージが出力されなくなります

  * **スクリプト終了エラー** の場合  
    **終了しないエラーとほぼ同じ挙動になり**、エラー発生時にも処理が継続するようになります  
    ただし、`try-catch`や`trap`でのエラートラップは引き続き可能です  
    コンソールにエラーメッセージは出力されません

* `Inquire` - エラー発生時に対話型のダイアログでユーザに動作を選択させます

#### 使い方
```PowerShell
$ErrorActionPreference = "Stop"
Write-Error "Error!"  #処理はここで終了します
Write-Output "End"
```

### `-ErrorAction`共通パラメータ

PowerShellの**コマンドレット**および**高度な関数**には`-ErrorAction`というパラメータが備わっています。このパラメータを使用するとコマンド単位でエラー発生時の挙動を変更できます。
`-ErrorAction`パラメータの指定値は`$ErrorActionPreference`自動変数での設定値よりも優先されます。

* `Continue` - デフォルトの挙動

* `Stop`
  * **終了しないエラー**の場合  
    挙動が**スクリプト終了エラーと同等に変更されます**
  
  * **ステートメント終了エラー**の場合  
    **ほぼ挙動は変化しません**が、例外的に関数呼び出し元で`$?`変数にセットされる値が変わるようです。（これが動作仕様なのかバグなのかは分かりません）

    ```PowerShell
    # ステートメント終了エラーが発生する関数
    function func1 {[CmdletBinding()]Param() 1 / 0 }
    
    func1  # -ErrorActionを指定せず呼び出し
    $?  #Trueになる

    func1 -ErrorAction Stop
    $?  #Falseに変わる
    ```
  
  * **スクリプト終了エラー** の場合 - **挙動は変化しません**

* `SilentlyContinue`
  * **終了しないエラー**の場合  
    エラーとしての挙動は変化しませんが、コンソールに**エラーメッセージが出力されなくなります**
  
  * **ステートメント終了エラー**の場合  
    **挙動は変化しません** コンソールにもエラーメッセージが表示されます
  
  * **スクリプト終了エラー** の場合  
    **終了しないエラーにSilentlyContinueを指定した場合とほぼ同じ挙動になります**
    エラー発生時にも**処理が継続します**  
    コンソールに**エラーメッセージは出力されません**
    ただし、`try-catch`や`trap`でのエラートラップは引き続き可能です  

* `Ignore`
  * **終了しないエラー**の場合  
    コンソールにエラーメッセージが出力されなくなり、`$Error`自動変数に**エラー情報が格納されなくなります**
    **エラー発生しなかったことになるわけではありません**
  
  * **ステートメント終了エラー**の場合  
    **挙動は変化しません** コンソールにもエラーメッセージが表示されますし、`$Error`自動変数にもエラー情報が格納されます
  
  * **スクリプト終了エラー** の場合  
    **終了しないエラーにSilentlyContinueを指定した場合とほぼ同じ挙動になります**
    コンソールにエラーメッセージは出力されませんが、`$Error`自動変数にはエラー情報が格納されます
    `try-catch`や`trap`でのエラートラップは引き続き可能です  
  
  > 補足：`-ErrorAction`に`Ignore`を指定できないバグについて
  > v6 以前の PowerShell には高度な関数に対して`-ErrorAction Ignore`を指定すると`NotSupportedException`（ステートメント終了エラー）が発生する[バグ](https://github.com/PowerShell/PowerShell/issues/1759)があります。このバグは PowerShell 7 で解消される予定です。

#### 使い方
```PowerShell
$ErrorActionPreference = "Stop"
# -ErrorActionPreference は $ErrorAction より優先されるため、
# 以下のコマンドでエラーが発生してもスクリプトは継続実行される
Get-Item -Path '存在しないパス' -ErrorAction 'Continue'
Write-Output "End"
```


## エラーロギング

### `$Error`自動変数
* いずれのエラーも、エラー発生時に`$Error`という自動変数に**エラー情報が格納**されます。  
* `$Error`は`ArrayList`型の配列になっていて、一番最後に発生したエラーが配列の先頭に格納されるスタック型です。
  よって**最も直近に発生したエラー情報には`$Error[0]`でアクセス**できます。

* 基本的にエラー発生時は必ず`$Error`自動変数にエラー情報が記録されます。`try-catch`によるエラートラップや、`$ErrorActionPreference = 'SilentlyContinue'`によるメッセージ出力抑制をした場合でも記録はされます。

* 唯一の例外は**終了しないエラーに`-ErrorAction Ignore`を組み合わせた場合**で、この場合は`$Error`に情報は記録されません。

* エラー情報は`[System.Management.Automation.ErrorRecord]`型オブジェクトで記録され、様々な情報が格納されています。

```PowerShell
Write-Error 'Error!!!'
$Error[0] | Select-Object *
```
```
writeErrorStream      : True
PSMessageDetails      : 
Exception             : Microsoft.PowerShell.Commands.WriteErrorException: Error!!!
TargetObject          : 
CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,test.ps1
ErrorDetails          : 
InvocationInfo        : System.Management.Automation.InvocationInfo
ScriptStackTrace      : <ScriptBlock>、F:\ErrorHandlingPS\test.ps1: 行 1
PipelineIterationInfo : {0, 0, 0}
```

よく使うであろう処理をいくつか紹介します

```PowerShell
# 発生した例外オブジェクトを取得
$Error[0].Exception

# 発生した例外の型名を取得
$Error[0].Exception.GetType().Fullname

# エラーメッセージを文字列で取得
$Error[0].Exception.Message

# エラースタックをクリア
$Error.Clear()
```
#### `Get-Error`コマンドレット
* PowerShell 7 Preview.5 から`Get-Error`というコマンドレットが実装されました

* このコマンドを引数なしで実行すると直近に発生したエラー情報1件が取得・表示できます
  内部処理的にはほぼ`$Error[0]`を呼び出しているだけです。

* 以下のように`-Newest`パラメータで指定件数のエラー情報を取得することもできます

```PowerShell
# 直近10件のエラーを取得
Get-Error -Newest 10

# 以下のコマンドと等価です
$Error[0..10]
```

* `$Error`自動変数を使えばいいので、下位互換性のことを考えるとあまり利用するシーンは無い気がします


### `$?`自動変数
`$?`自動変数には、**直前に実行した処理が成功したか否か**が`[bool]`型で記録されます。

* `True` - 直前の処理で**エラーが発生しなかった**ことを示します

* `False` - 直前の処理で**何らかのエラーが発生した**ことを示します

ただし、`$?`の指す「直前に実行した**処理**」というのが、一体なにを指すのか**明確に定義されていない**ため、直感的に予想する結果と異なる結果になることが少なくありません。  

#### `$?`が`False`になる例
```PowerShell
Write-Error 'Error' #終了しないエラー
$?  #False

[int]::Parse('hoge')  #ステートメント終了エラー
$?  #False

try {
    throw  #スクリプト終了エラーをtry-catch (catchブロックが空)
}
catch { }
$?  #False
```

ここまでは直感的に理解できる結果だと思います

#### `$?`が直感と異なる結果になる例
```PowerShell
# 1.終了しないエラー + "-ErrorAction Ignore" 指定
Write-Error 'Error' -ErrorAction Ignore
$?  #False

# 2.終了しないエラーを(...)でラップ
(Write-Error 'Error')
$?  #FalseになりそうだけどTrue

# 3.ステートメント終了エラーを(...)でラップ
([int]::Parse('hoge'))
$?  #終了しないエラーはTrueだったのにステートメント終了エラーはFalse

# 4.条件分岐の判定式内での終了しないエラー
if (Write-Error 'Error') { }
$?  #True

# 5. 関数内での終了しないエラー
function function1 { Write-Error 'Error' }
function1
$?  #True

# 6. 関数内でのステートメント終了エラー
function function2 { [int]::Parse('hoge') }
function2
$?  #True
```

あえてかなり嫌らしい挙動を示す例を挙げています。

* 1.は`-ErrorAction`パラメータに`Ignore`を指定してもエラーが無かったことになるわけではないことを示します。
  メッセージも表示されないし`$Error`に記録もされませんが、**エラーは確かに発生した**のです。

* 2.の挙動は**特に理解し難い**です。
  PowerShellでは他言語と異なり`()`自体が式として定義されています。`()`式はその中の処理を実行した後、その実行結果を出力する動作をします。すなわち`$?`の直前に実行された処理は`()`内の処理ではなく、**`()`式が実行結果を出力する処理**であるため、`$?`は`True`を返すとする解釈になります。

* 3.については`(...)`までが一つのステートメントとして扱われるため、内部でステートメント終了エラーが発生することで外側の`(...)`まで含めエラー終了と判定されると解釈します。

* 4.は**条件分岐の判定式内の終了しないエラーは`$?`自動変数に反映しない**という特例です。
  なお、例示はしていませんが、判定式内の**ステートメント終了エラーは**`$?`に反映されます。

* 5.および6.の挙動は**私は理解できていません、現状そうなるということで紹介しておきます**
  自作関数の場合は`$PSCmdlet.WriteError()`や`$PSCmdlet.ThrowTerminatingError()`を使うことでこの謎挙動を回避することができます（詳細は後述）

> 余談：
> PowerShellには`$?`の他に`$LASTEXITCODE`という自動変数がありますが、
> これは外部コマンドの終了コードが格納される変数であり、`$?`とは明確に異なるものです。


## エラーを発生させる方法
### 終了しないエラー
#### `Write-Error`コマンドレット

* 終了しないエラーを発生させる最も一般的な方法は`Write-Error`コマンドレットを使う方法です

```PowerShell
# 1.エラーメッセージを文字列で与える方法
# この場合の例外型は [Microsoft.PowerShell.Commands.WriteErrorException] になる
Write-Error -Message "エラーメッセージ"   # '-Message' は省略可能

# 2.任意の[Exception]を継承したオブジェクトを与える方法
$ex = [System.Exception]::new()
Write-Error -Exception $ex

# 3. 任意の[System.Management.Automation.ErrorRecord]オブジェクトを与える方法
$er = [System.Management.Automation.ErrorRecord]::new(
    'exception message',
    'errorId',
    [System.Management.Automation.ErrorCategory]::InvalidResult,
    $null
)
Write-Error -ErrorRecord $er
```

* ただし、`Write-Error`は関数内などで使用した場合に、その**呼び出し元スコープで`$?`自動変数が正しく設定されない**という問題があります。これは`Write-Error`の代わりに`$PSCmdlet.WriteError()`メソッドを使うことで回避できますが、**高度な関数**でしか使えない方法になります。

```PowerShell
function function1 {
  [CmdletBinding()]
  param()
  $er = [System.Management.Automation.ErrorRecord]::new(
    'exception message',
    'errorId',
    [System.Management.Automation.ErrorCategory]::InvalidResult,
    $null
  )
  $PSCmdlet.WriteError($er)  #Write-Errorの代わりに使う
}

function1
$?  #False (Write-Errorを使った場合はTrueになる)
```

### ステートメント終了エラー

#### `$PSCmdlet.ThrowTerminatingError`メソッド

* 任意のステートメント終了エラーを明示的に発生させる唯一の方法と考えられています

* **高度な関数内でのみ使用可能**です

```PowerShell
function function2 {
  [CmdletBinding()]
  param()
  $er = [System.Management.Automation.ErrorRecord]::new(
    'exception message',
    'errorId',
    [System.Management.Automation.ErrorCategory]::InvalidResult,
    $null
  )
  $PSCmdlet.ThrowTerminatingError($er)
}

function2
```

### スクリプト終了エラー

#### `throw`ステートメント

* スクリプト終了エラーを発生させる最も一般的で簡潔な方法です

```PowerShell
# 引数なしで呼び出した場合、エラーメッセージは"ScriptHalted"
# 例外型は[System.Management.Automation.RuntimeException]
throw

# 任意のオブジェクトを渡した場合、エラーレコードのTargetObjectに格納される
throw 1234
$Error[0].TargetObject  #1234

# [Exception]型を継承したオブジェクトを渡した場合、その例外がthrowされる
throw [System.Management.Automation.ItemNotFoundException]::new()
$Error[0].Exception.GetType().Name  #ItemNotFoundException
```


## Pipeline Chain Operators
PowerShell 7 Preview.5 から実装された **Pipeline Chain Operators** を紹介しておきます。Pipeline Chain Operators のエラー処理は PowerShell-RFC にも[詳しく記載されている](https://github.com/PowerShell/PowerShell-RFC/blob/master/4-Experimental-Accepted/RFC0046-Chain-Operators.md#pipelines)ので併せて読んでいただくのが良いかと思います。

なお、Pipeline Chain Operators は現在 Experimental の扱いなので、以下コマンドで有効化しないと使えないかもしれません。（Preview 版の PowerShell は既定で有効になっています）

```PowerShell
Enable-ExperimentalFeature -Name PSPipelineChainOperators
```

### `&&`演算子

* `&&`で2つ以上のパイプラインをつなげると、前のパイプラインが**成功**した場合のみ、次のパイプラインが実行されます

```PowerShell
# すべてのパイプラインが成功する
Write-Output 'AAA' && Write-Output 'BBB' && Write-Output 'CCC'
```
```
AAA
BBB
CCC
```

```PowerShell
# 2つめのパイプラインで終了しないエラーが発生するので3つめのパイプラインは実行されない
Write-Output 'AAA' && Write-Error 'ERROR' && Write-Output 'CCC'
```
```
AAA
Write-Output 'AAA' && Write-Error 'ERROR' && Write-Output 'CCC' : ERROR
+ CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException
```

### `||`演算子

* `||`で2つ以上のパイプラインをつなげると、前のパイプラインが**失敗**した場合のみ、次のパイプラインが実行されます

```PowerShell
# 最初のパイプラインが成功するので2つめのパイプラインは実行されない
Write-Output 'AAA' || Write-Output 'BBB'
```
```
AAA
```

```PowerShell
# 最初のパイプラインで終了しないエラーが発生するので2つめのパイプラインが実行される
Write-Error 'ERROR' || Write-Output 'BBB'
```
```
Write-Error 'ERROR' || Write-Output 'BBB' : ERROR
+ CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException
BBB
```

### Pipeline Chain Operators におけるエラーハンドリング

* `&&`演算子も`||`演算子も、パイプラインが成功したか失敗したかの判定に`$?`自動変数を使っています
  つまり各演算子は以下のコードと等価です

```PowerShell
# Write-Output 'AAA' && Write-Output 'BBB' と同等のコード
Write-Output 'AAA'
if ($?) { Write-Output 'BBB' }

# Write-Error 'ERROR' || Write-Output 'BBB' と同等のコード
Write-Error 'ERROR'
if (-not $?) { Write-Output 'BBB' }
```

* 前述のように`$?`演算子は直感的でない結果を返す場合があるため、 Pipeline Chain Operators を使ったコードも直感と異なる挙動を示すことがあります

```PowerShell
# 2つめのパイプラインは実行されない
(Write-Error 'ERROR') || Write-Output 'BBB'

#ステートメント終了エラーが発生する関数
function f1 { [CmdletBinding()]param() 1 / 0 }

# 2つめのパイプラインは実行されない
f1 || Write-Output 'BBB'

# 2つめのパイプラインは実行される
f1 -ErrorAction Stop || Write-Output 'BBB'
```

* スクリプト終了エラーが発生した場合、Pipeline Chain Operators は実行されません

```PowerShell
# 2つめのパイプラインは実行されない
throw || Write-Output 'BBB'
```

* `try-catch`や`trap`によりエラートラップされた場合、Pipeline Chain Operators は実行されることなく`catch`や`trap`ブロックに処理が移行します

```PowerShell
try {
  # 2つめのパイプラインは実行されない
  [int]::Parse('hoge') || Write-Output 'BBB'
}
catch {
  Write-Output 'Catch'
}
```

* bashに習熟しているとエラーハンドリングとして `||`とフロー制御式（`return`や`exit`）を組み合わせて使いたくなるかもしれませんが、PowerShellにおいてフロー制御式はパイプラインではないため、Pipeline Chain Operators の左右に置くことはできません

```PowerShell
# Get-Itemコマンドが失敗した場合にexitで処理を打ち切りたいとする

# これはエラーになる
Get-Item -Path 'hoge' || exit 1

# 部分式演算子$()を使えば回避することは可能
Get-Item -Path 'hoge' || $( exit 1 )

# PowerShell v6 以前でも使える等価な書き方
Get-Item -Path 'hoge'
if (-not $?) {
  exit 1
}
```


## ベストプラクティス

PowerShell 7 で Pipeline Chain Operators が登場したことで自作関数を書く際のカスタムエラーの出力方法をどうすればいいのか、かなりややこしくなってきました....
挙動が変わる可能性もあるので、あくまで**暫定**でこうすると良さそうだ、というレベルで書いておきます。

### 関数を書くときは必ず高度な関数にする
* その場で書き捨てするような場合を除き、PowerShell で関数を書くのであれば必ず**高度な関数**で書きましょう

* 高度な関数というのは冒頭に`[CmdletBinding()]`が付いているやつです

* 高度な関数にしないと`-ErrorAction`共通パラメータが使えません

```PowerShell
# 普通の関数
function Normal-Function {
  Write-Output 'Hello'
}

# 高度な関数
function Advanced-Function {
  [CmdletBinding()]
  param()
  Write-Output 'Hello'
}
```

### 自作関数では終了しないエラーを出す

* PowerShell から呼び出すための関数であれば、その関数が出すエラーは基本的に**すべて 終了しないエラー としてしまう**のが良いかと思います。呼び出し元からすると最もエラーハンドリングの融通が効くので。

* エラー発生時に処理を打ち切りたければ`Write-Error`実行直後に`return`で終了してしまえばいいです

* 関数内で別のコマンドや.NETメソッドを呼び出す際はなるべく`try-catch`を使い、終了しないエラー以外のエラーが発生しないように気をつけます

* 関数が Pipeline Chain Operators と組み合わせて呼び出される可能性がある場合は、`Write-Error`は使わず`$PSCmdlet.WriteError()`を使いましょう

### `$?`および Pipeline Chain Operators は使わない

* `$?`を信用してはいけません

* ただし自作関数が自分以外の誰かによって呼ばれる可能性がある場合、これらを使われないとは限りません
  外部公開するモジュールを作成している場合などは考慮しておく必要があるでしょう

### `throw`はなるべく使わない

* 特に関数内では`throw`を不用意に使わないように。呼び出し元まで巻き込みます

* 関数内でどうしても終了するエラーを出したい場合は`$PSCmdlet.ThrowTerminatingError()`でステートメント終了エラーを使いましょう

* （そのような状況があるのか分かりませんが）関数内でやむを得ず`throw`を使う場合、必ず`throw`実行直後に`return`などで明示的に処理をを終了します
  これは`-ErrorAction SilentlyContinue`を指定された場合に`throw`以後の処理が意図せず実行されることを防ぐためです
  （`throw`の代わりに`$PSCmdlet.ThrowTerminatingError()`を使えばこの状況も回避可能です）

```PowerShell
function Invoke-Throw {
  [CmdletBinding()]
  Param()
  #なにか致命的なエラーが発生してthrowで処理を打ち切りたくなったとする
  throw '致命的なエラー！'
  
  # throwで処理が終わるだろうと思い込んで後続に処理を書いてしまうと...
  Write-Output '後続処理'
}

# 関数がこのように呼び出だされた時、関数内の'後続処理'は実行されてしまう
Invoke-Throw -ErrorAction SilentlyContinue
```