Share via


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

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

注意

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

基本術語

我們需要先涵蓋一些基本詞彙,才能進入這個字詞。

例外狀況

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

Throw 和 Catch

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

呼叫堆疊

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

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

終止和非終止錯誤

例外狀況通常是終止錯誤。 擲回的例外狀況會被攔截或終止目前的執行。 根據預設,會產生非終止錯誤,而且它會將錯誤 Write-Error 新增至輸出數據流,而不會擲回例外狀況。

我指出這一點,因為 Write-Error 和其他非終止錯誤不會觸發 catch

吞下例外狀況

這是當您攔截錯誤只是為了隱藏錯誤時。 請謹慎執行此動作,因為它可能會使疑難解答問題變得非常困難。

基本命令語法

以下是 PowerShell 中使用的基本例外狀況處理語法快速概觀。

Throw

若要建立自己的例外狀況事件,我們會擲回具有 關鍵詞的例外狀況 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 Stop

我提到, 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

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例外狀況資訊。

Try/Finally

有時候,您不需要處理錯誤,但在發生例外狀況時仍需要執行一些程序代碼。 腳本 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

catch 起使用和 finally 一起使用是完全有效的。 您大部分時間都會使用其中一個或另一個,但您可能會發現同時使用這兩者的情況。

$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 但這是針對 Managed 程式代碼的呼叫。

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 )

只有在從 Managed 程式代碼擲回事件時,您才會取得此堆疊追蹤。 我直接呼叫 .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]"
}

/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 例外狀況的大清單

我利用 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 之後,它會變成非終止錯誤。 這會讓函式的呼叫端負擔決定如何處理錯誤。 他們可以使用 或從 內try{...}catch{...}呼叫它,將其重新轉換成終止錯誤-ErrorAction Stop

公用函式範本

最後一個採取的方式,我與柯克蒙羅的談話是,他把一個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 Goto 例外狀況處理程式。