共用方式為


您想要知道關於例外狀況的一切

錯誤處理只是撰寫程式代碼時的一部分。 我們通常會檢查並驗證預期行為的條件。 當非預期的情況發生時,我們會轉向例外狀況處理。 您可以輕鬆地處理其他人程式代碼所產生的例外狀況,也可以為其他人產生自己的例外狀況來處理。

備註

本文的原始版本出現在@KevinMarquette撰寫的部落格上。 PowerShell 小組感謝 Kevin 與我們分享此內容。 請在 PowerShellExplained.com查看他的部落格。

基本術語

我們需要先涵蓋一些基本詞彙,才能進入主題。

例外

例外狀況就像一般錯誤處理無法處理問題時所建立的事件。 嘗試將數位除以零或記憶體不足是造成例外狀況的範例。 有時候,您所使用的程式代碼作者會在發生特定問題時建立例外狀況。

Throw 和 Catch

發生例外狀況時,我們會說擲回例外狀況。 若要處理擲回的例外狀況,您需要攔截它。 如果擲回例外狀況,而且它未被某個項目攔截,腳本就會停止執行。

呼叫堆疊

呼叫堆疊是彼此呼叫的函式清單。 呼叫函式時,它會新增至堆疊或清單頂端。 當函式結束或傳回時,它會從堆疊中移除。

擲回例外狀況時,會檢查該呼叫堆疊,以便讓例外狀況處理程序攔截它。

終止和非終止錯誤

例外狀況通常是終止錯誤。 拋出的例外會被攔截或終止目前的執行。 根據預設,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 StopWrite-Error 會產生終止錯誤,可以使用 catch 來進行處理。

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

李戴利,感謝你提醒我們這樣使用-ErrorAction Stop

Cmdlet -ErrorAction 停止

如果您在任何進階函式或 Cmdlet 上指定 -ErrorAction Stop,它會將所有 Write-Error 語句變成會停止執行的終止錯誤,或者可以由 catch 處理的終止錯誤。

Start-Something -ErrorAction Stop

如需 ErrorAction 參數的詳細資訊,請參閱 about_CommonParameters。 如需 $ErrorActionPreference 變數的詳細資訊,請參閱 about_Preference_Variables

嘗試/捕捉

在 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,有一個類型的ErrorRecord自動變數 ($PSItem$_) 包含例外狀況的詳細數據。 以下是一些重要屬性的快速概觀。

在這些範例中,我使用 無效的路徑 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

此處的重要詳細數據會顯示 ScriptNameLine 程序代碼的 ,以及 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."

如果 上沒有設定訊息,則這也是在呼叫 $PSItem.ToString() 時所傳回的 ErrorRecord訊息。

$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 區塊的例外狀況類型,直到找到符合例外狀況的區塊為止。 請務必瞭解例外狀況可以繼承自其他例外狀況。 在上述範例中, FileNotFoundException 繼承自 IOException。 因此,如果 IOException 是第一個,就會改為呼叫它。 即便有多個相符條件,仍然只會叫用一個 catch 區塊。

如果我們有 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 ,我們仍然可以根據例外狀況類型處理錯誤。 使用 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 例外狀況的大清單

我利用 Reddit r/PowerShell 社群的協助編譯了主要清單,其中包含數百個 .NET 例外狀況來補充這篇文章。

我從搜尋該清單尋找那些感覺適合我情況的例外狀況開始。 您應該嘗試在基底 System 命名空間中使用例外狀況。

例外是物件

如果您開始使用許多具類型的例外狀況,請記住這些例外狀況是物件。 不同的例外狀況有不同的建構函式和屬性。 如果我們查看 System.IO.FileNotFoundExceptionFileNotFoundException 文件,我們會看到我們可以傳入一條訊息和一個檔案路徑。

[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)
}

這會將錯誤的來源更改為 Cmdlet,並將函式的內部細節隱藏起來,讓 Cmdlet 的使用者無法看到。

可能會嘗試產生終止錯誤

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() 的細微差異之一在於它會在您的 Cmdlet 中創建一個終止錯誤,但當它離開 Cmdlet 後,它會變成非終止錯誤。 這樣會讓函式的呼叫者來承擔決定如何處理錯誤的責任。 他們可以使用-ErrorAction Stop或從try{...}catch{...}內呼叫它,將其轉換成終止錯誤。

公用函式範本

我最後的一個體會是,從我與柯克·蒙羅(Kirk Munro)的對話中學到的是,在他所有的進階功能中,他會把try{...}catch{...}放在每一個beginprocessend區塊周圍。 在這些一般捕捉區塊中,他有一條單行用來 $PSCmdlet.ThrowTerminatingError($PSItem) 處理從他的函式中拋出的所有例外。

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

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

由於所有內容都在他的函數語句try中,因此一切都能一致運作。 這也為最終使用者提供易於理解的錯誤信息,以隱藏錯誤信息中的內部代碼。

陷阱

我專注於 try/catch 例外狀況的層面。 但是,在我們總結這一點之前,我需要提及一個舊版功能。

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 來在程式碼中產生錯誤。 我也會接受 Kirk 的建議,將 ThrowTerminatingError 設為每個函式的預設例外處理程式。