Поделиться через


Все, что вы хотели знать об исключениях

Обработка ошибок является лишь частью жизни, когда речь идет о написании кода. Мы часто проверяем и подтверждаем условия для ожидаемого поведения. Когда происходит неожиданное, мы обратимся к обработке исключений. Вы можете легко обрабатывать исключения, созданные кодом других людей, или создавать собственные исключения для других пользователей для обработки.

Замечание

Оригинал этой статьи впервые был опубликован в блоге автора @KevinMarquette. Команда PowerShell благодарит Кевина за предоставление этого содержимого нам. Читайте его блог — PowerShellExplained.com.

Базовая терминология

Мы должны охватывать некоторые основные термины, прежде чем мы переходим к этому.

Исключение

Исключение — это как событие, которое создается, когда стандартная обработка ошибок не может справиться с проблемой. Попытка деления числа на нуль или нехватки памяти — это примеры того, что создает исключение. Иногда автор кода, который вы используете, создает исключения для некоторых проблем при их возникновении.

Подача и ловля

При возникновении исключения мы говорим, что исключение выбрасывается. Чтобы обработать исключение, необходимо поймать его. Если выбрасывается исключение, и оно не перехватывается чем-либо, скрипт перестает выполняться.

Стек вызовов

Стек вызовов — это список функций, которые вызвали друг друга. При вызове функции он добавляется в стек или верхнюю часть списка. Когда функция выходит или возвращается, она удаляется из стека.

При возникновении исключения стек вызовов проверяется, чтобы обработчик исключений мог его перехватить.

Фатальные и нефатальные ошибки

Исключение обычно является завершающим ошибкой. Возникающее исключение либо перехватывается, либо завершается текущее выполнение. По умолчанию 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 Stop, Write-Error создает завершающую ошибку, которую можно обработать с помощью catch.

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

Спасибо Ли Дейли за напоминание об использовании -ErrorAction Stop таким образом.

Cmdlet -ErrorAction Stop

Если указать -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 можно получить с помощью переменной $_ .

Попробовать/Наконец

Иногда вам не нужно обрабатывать ошибку, но вам все равно нужен код для выполнения, если исключение происходит или нет. Сценарий 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 выполняется каждый раз.

Так как вы не перехватываете исключение, оно по-прежнему распространяется по стеку вызовов.

Попробовать/Поймать/Наконец

Это совершенно допустимо, чтобы использовать catch и finally вместе. Большую часть времени вы будете использовать один или другой, но вы можете найти сценарии, где вы используете оба.

$PSItem

Теперь, когда мы разобрались с основами, можем углубиться в детали.

catch В блоке есть автоматическая переменная ($PSItemили$_) типаErrorRecord, содержащая сведения об исключении. Ниже приведен краткий обзор некоторых ключевых свойств.

В этих примерах для создания этого исключения использовался недопустимый путь 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

Важные сведения здесь показывают ScriptName, Line код и 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 пространстве имен.

Исключения — это объекты

Если вы начинаете использовать много типизированных исключений, помните, что они являются объектами. Разные исключения имеют разные конструкторы и свойства. Если мы рассмотрим документацию для FileNotFoundExceptionSystem.IO.FileNotFoundException, мы видим, что мы можем передать сообщение и путь к файлу.

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

Интересно, что мы можем вызвать throw изнутри catch , и он повторно создает текущее исключение.

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.

Если в сообщении об ошибке говорится, что мой сценарий не работает, потому что я вызвал throw в строке 31, это плохое сообщение, которое увидят пользователи вашего скрипта. Это не говорит им ничего полезного.

Dexter Dhami отметил, что я могу использовать 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 функцию в качестве источника проблемы? Это сообщает пользователю что-то полезное.

Потому что $PSItem — это ErrorRecord, мы также можем использовать ThrowTerminatingError таким образом, чтобы снова выбросить.

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

Это изменяет источник ошибки на командлет и скрывает внутренние компоненты вашей функции от пользователей командлета.

Попробуйте создать конечные ошибки

Кирк Мунро указывает, что некоторые исключения являются только завершающимися ошибками при выполнении внутри 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() заключается в том, что он создает завершающую ошибку в командлете, но она превращается в незавершающую ошибку после выхода из командлета. Это оставляет нагрузку на вызывающий объект функции, чтобы решить, как обрабатывать ошибку. Они могут превратить его обратно в завершающееся сообщение об ошибке, используя -ErrorAction Stop или вызывая ее изнутри try{...}catch{...}.

Общедоступные шаблоны функций

Последний вывод, который я сделал из разговора с Кирком Мунро, это то, что он помещает try{...}catch{...} вокруг каждого begin, process и end блока в своих продвинутых функциях. В этих универсальных блоках перехвата у него есть одна строка с использованием $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 для генерации ошибок в моем коде. Я также собираюсь последовать совету Кирка и сделать ThrowTerminatingError моим обработчиком исключений goto для каждой функции.