在编写代码时,错误处理只是生命的一部分。我们通常可以检查并验证预期行为的条件。当意外发生时,我们转向异常处理。您可以轻松处理其他人代码生成的异常,或者您可以为其他人生成自己的异常来处理。

指数

基本术语

我们需要在跳入这个之前涵盖一些基本术语。

例外

异常就像在正常错误处理无法处理问题时创建的事件。尝试将数字除以零或耗尽内存是创建异常的示例。有时,您正在使用的代码的作者将在发生某些问题时创建异常。

扔和抓住

当发生异常时,我们说抛出异常。要处理抛出异常,您需要抓住它。如果抛出异常并且它没有被某些东西捕获,则脚本将停止执行。

呼叫堆栈

调用堆栈是互相调用的函数列表。调用函数时,它会添加到堆栈或列表的顶部。当函数退出或返回时,将从堆栈中删除。

当抛出异常时,检查调用堆栈以便捕获异常处理程序。

终止和非终止错误

An exception is generally a terminating error. A thrown exception will either be caught or it will terminate the current execution. By default, a non-terminating error is generated by Write-Error and it adds an error to the output stream without throwing an exception.

I point this out because Write-Error and other non-terminating errors will not trigger the catch.

吞咽一个例外

这是捕获错误以抑制它的情况。谨慎执行此操作,因为它可以使问题进行故障排除问题非常困难。

基本命令语法

以下是PowerShell中使用的基本异常处理语法的快速概述。

To create our own exception event, we throw an exception with the throw keyword.

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

This creates a runtime exception that is a terminating error. It will be handled by a catch in a calling function or exit the script with a message like this.

PS:> Do-Something

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

写错-Erroraction停止

I mentioned that Write-Error does not throw a terminating error by default. If you specify -ErrorAction Stop then Write-Errorgenerates a terminating error that can be handled with a catch.

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

Thank you to Lee Daily for reminding about using -ErrorAction Stop this way.

Cmdlet -Erroraction停止

If you specify -ErrorAction Stop on any advanced function or Cmdlet, it will turn all Write-Error statements into terminating errors that will stop execution or that can be handled by a catch.

Do-Something -ErrorAction Stop

试着抓

The way exception handling works in PowerShell (and many other languages) is that you first try a section of code and if it throws an error, you can catch it. Here is a quick sample.

try
{
    Do-Something
}
catch
{
    Write-Output "Something threw an exception"
}

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

The catch script only runs if there is a terminating error. If the try executes correctly, then it will skip over the catch.

尝试/最后

Sometimes you don’t need to handle an error but still need some code to execute if an exception happens or not. A finally script does exactly that.

看看这个例子:

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

Any time you open or connect to a resource, you should close it. If the ExecuteNonQuery() throws an exception, the connection will not get closed. Here is the same code inside a try/finally block.

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

In this example, the connection will get closed if there is an error. It will also get closed if there is no error. The finally script will run every time.

因为您没有捕获异常,所以它仍将传播调用堆栈。

尝试/捕获/最后

It is perfectly valid to use catch and finally together. Most of the time you will use one or the other, but you may find scenarios where you will use both.

$ psitem.

既然我们就淘汰了基础,我们就可以挖得更深。

Inside the catch block, there is an automatic variable ($ psitem. or $_) of type ErrorRecord that contains the details about the exception. Here is a quick overview of some of the key properties.

For these examples, I used an invalid path in ReadAllText to generate this exception.

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

psitem.tostring()

This will give you the cleanest message to use in logging and general output. ToString() is automatically called if $ psitem. is placed inside a string.

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

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

$ psitem.invocationInfo.

This property contains additional information collected by PowerShell about the function or script where the exception was thrown. Here is the InvocationInfo from the sample exception that I created.

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

The important details here show the ScriptName, the Line of code and the ScriptLineNumber where the invocation started.

$ psitem.scriptstacktrace.

此属性将显示函数调用的顺序,使您可以获得生成异常的代码。

PS:> $ psitem.scriptstacktrace.
at Get-Resource, C:\blog\throwerror.ps1: line 13
at Do-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."

This is also the message returned when calling $ psitem..ToString() if there was not one set on the ErrorRecord.

$ psitem.exception.innerexception.

异常可以包含内部异常。当您调用的代码捕获异常并抛出不同的异常时,这通常是这种情况。他们会将原始异常放在新的例外内。

PS:> $ psitem.exception.innerexception.Message
The network path was not found.

当我谈论重新投掷异常时,我会稍后重新审视这一点。

$ psitem.exception.stacktrace.

This is the StackTrace for the exception. I showed a ScriptStackTrace above, but this one is for the calls to managed code.

at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
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
{
    Do-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"
}

The exception type is checked for each catch block until one is found that matches your exception. It is important to realize that exceptions can inherit from other exceptions. In the example above, FileNotFoundException inherits from IOException. So if the IOException was first, then it would get called instead. Only one catch block will be invoked even if there are multiple matches.

If we had a System.IO.PathTooLongException then the IOException would match but if we had a InsufficientMemoryException then nothing would catch it and it would propagate up the stack.

立即捕获多种类型

It is possible to catch multiple exception types with the same catch statement.

try
{
    Do-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建议这个增加。

投掷类型的例外

You can throw typed exceptions in PowerShell. Instead of calling throw with a string:

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")

If you are not yet using PowerShell 5.0, you will have to use the older New-Object approach.

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

通过使用键入的异常,您(或其他)可以通过上一节中提到的类型来捕获异常。

写入错误 - 异常

We can add these typed exceptions to Write-Error and we can still catch the errors by exception type. Use Write-Error like in these examples:

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

# With message inside new exception
写入错误 - 异常 ([System.IO.FileNotFoundException]::new("Could not find path: $path")) -ErrorAction Stop

# Pre PS 5.0
写入错误 - 异常 ([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异常来补充这篇文章。

I start by searching that list for exceptions that feel like they would be a good fit for my situation. You should try to use exceptions in the base System namespace.

例外是对象

如果您开始使用大量键入的异常,请记住它们是对象。不同的异常具有不同的构造函数和属性。如果我们看看 文件 for System.IO.FileNotFoundException, we will see that we can pass in a message and a file path.

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

And it has a FileName property that exposes that file path.

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

你必须咨询 .NET文档 对于其他构造函数和对象属性。

重新投掷一个例外

If all you are going to do in your catch block is throw the same exception, then don’t catch it. You should only catch an exception that you plan to handle or perform some action when it happens.

有时需要在异常上执行动作,但重新抛出异常,所以下游的东西可以处理它。我们可以写一条消息或记录靠近我们发现它的问题,但是处理堆栈的问题。

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

Interestingly enough, we can call throw from within the catch and it will re-throw the current exception.

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

我们希望重新抛出异常以保留原始执行信息,如源脚本和行号。如果我们在这一点上抛出一个新的例外,它将隐藏出例所开始的。

重新投掷一个新的例外

If you catch an exception but you want to throw a different one, then you should nest the original exception inside the new one. This allows someone down the stack to access it as the $ psitem.exception.innerexception..

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

$ pscmdlet.throwterminatingerror()

The one thing that I don’t like about using throw for raw exceptions is that the error message points at the throw statement and indicates that line is where the problem is.

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.

Having the error message tell me that my script is broken because I called throw on line 31 is a bad message for users of your script to see. It does not tell them anything useful.

Dexter Dhami pointed out that I can use 扔TerminatingError() to correct that.

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

If we assume that 扔TerminatingError() was called inside a function called Get-Resource, then this is the error that we would see.

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

Do you see how it points to the Get-Resource function as the source of the problem? That tells the user something useful.

Because $ psitem. is an ErrorRecord, we can also use 扔TerminatingError this way to re-throw.

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

这将将错误的源更改为cmdlet,并隐藏来自cmdlet的用户的功能内部。

尝试可以创建终止错误

Kirk Munro指出,一些例外只是在尝试/ Catch块内执行时终止错误。这是他给我的例子,它通过零运行时异常生成分割。

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

然后调用它,以便看到它生成错误并仍会输出消息。

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

但是通过在尝试/捕获内部放置相同的代码,我们看到了其他事情发生。

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

我们看到错误成为终止错误,而不是输出第一条消息。我不喜欢这个的是,你可以在函数中拥有这个代码,如果有人使用尝试/ catch,它将不同。

我自己并没有遇到这个问题,但它是有意识的角落案。

$ pscmdlet.throwterminatingerror()内部尝试/捕获

One nuance of $ pscmdlet.throwterminatingerror() is that it creates a terminating error within your Cmdlet but it turns into a non-terminating error after it leaves your Cmdlet. This leaves the burden on the caller of your function to decide how to handle the error. They can turn it back into a terminating error by using -ErrorAction Stop or calling it from within a try{...}catch{...}.

公共功能模板

One last take a way I had with my conversation with Kirk Munro was that he places a try{...}catch{...} around every begin, process and end block in all of his advanced functions. In those generic catch blocks, he as a single line using $PSCmdlet.ThrowTerminatingError($PSitem) to deal with all exceptions leaving his functions.

function Do-Something
{
    [cmdletbinding()]
    param()

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

Because everything is in a try statement within his functions, everything acts consistently. This also gives clean errors to the end user that hides the internal code from the generated error.

陷阱

我专注于例外的尝试/捕获方面。但是在我们将其包装之前,我需要提及一个遗留功能。

A trap is placed in a script or function to catch all exceptions that happen in that scope. When an exception happens, the code in the trap will get executed and then the normal code will continue. If multiple exceptions happen, then the trap will get called over and over.

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

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

我个人从未采用这种方法,但我可以看到admin或控制器脚本中的值,它将记录任何和所有例外,然后仍然继续执行。

结束语

为您的脚本添加正确的异常处理不仅会使它们更稳定,但它也将更轻松地解决这些例外问题。

I spent a lot of time talking throw because it is a core concept when talking about exception handling. PowerShell also gave us Write-Error that handles all the situations where you would use throw. So don’t think that you need to be using throw after reading this.

Now that I have taken the time to write about exception handling in this detail, I am going to switch over to using Write-Error -Stop to generate errors in my code. I am also going to take Kirk’s advice and make 扔TerminatingError my goto exception handler for every funciton.