Trap exception ошибка

Продолжаем тему обработки ошибок в PowerShell, начатую в предыдущей статье. Сегодня речь пойдет об обработке прерывающих ошибок (исключений). Надо понимать, что сообщение об ошибке — это не то же самое, что исключение. Как вы помните, в PowerShell есть два типа ошибок — непрерывающие и прерывающие.

Непрерывающие ошибки позволяют продолжить работу, тогда как прерывающие останавливают выполнение команды. Остановка приводит к исключению (exception), которое и можно отлавливать и обрабатывать.

Примечание. Таким же образом можно обрабатывать и непрерывающие ошибки. Изменение параметра ErrorAction на Stop прервет выполнение команды и произведет исключение, которое можно уловить.

Для наглядности сгенерируем ошибку, попытавшись прочитать файл, на который у нас нет прав. А теперь обратимся к переменной $Error и выведем данные об исключении. Как видите, данное исключение имеет тип UnauthorizedAccessException и относится к базовому типу System.SystemException.

определение исключения

Для обработки исключений в PowerShell есть несколько способов, которые мы сегодня и рассмотрим.

Try/Catch/Finally

Конструкция Try/Catch/Finally предназначена для обработки исключений, возникающих в процессе выполнения скрипта. В блоке Try располагается исполняемый код, в котором должны отслеживаться ошибки. При возникновении в блоке Try прерывающей ошибки оболочка PowerShell ищет соответствующий блок Catch для обработки этой ошибки, и если он найден, то выполняет находящиеся в нем инструкции. Блок Catch может включать в себя любые команды, необходимые для обработки возникнувшей ошибки иили восстановления дальнейшей работы скрипта.

Блок Finally располагается обычно в конце скрипта. Команды, находящиеся в нем, выполняются в любом случае, независимо от возникновения ошибок. Была ли  ошибка перехвачена и обработана блоком Catch или при выполнении скрипта ошибок не возникало вообще, блок Finally будет выполнен. Присутствие этого блока в скрипте необязательно, основная его задача — высвобождение ресурсов (закрытие процессов, очистка памяти и т.п.).

В качестве примера в блок Try поместим код, который читает файлы из указанной директории. При возникновении проблем блок Catch выводит сообщение об ошибке, после чего отрабатывает блок Finally и работа скрипта завершается:

try {
Get-Content -Path ″C:Files*″ -ErrorAction Stop
}
catch {
Write-Host ″Some error was found.″
}
finally {
Write-Host ″Finish.″
}

обработка общей ошибки с помощью trycatch

Для блока Catch можно указать конкретный тип ошибки, добавив после ключевого слова Catch в квадратных скобках название исключения. Так в следующем примере укажем в качестве типа исключение System.UnauthorizedAccessException, которое возникает при отсутствии необходимых прав доступа к объекту:

try {
Get-Content -Path ″C:Files*″ -ErrorAction Stop
}
catch [System.UnauthorizedAccessException]
{
Write-Host ″File is not accessible.″
}
finally {
Write-Host ″Finish.″
}

обработка конкретного исключения в trycatch

Если для блока Catch указан тип ошибки, то этот блок будет обрабатывать только этот тип ошибок, или тип ошибок, наследуемый от указанного типа. При возникновении другого типа ошибка не будет обработана. Если же тип не указан, то блок будет обрабатывать любые типы ошибок, возникающие в блоке Try.

Блок Try может включать несколько блоков Catch, для разных типов ошибок, соответственно можно для каждого типа ошибки задать свой обработчик. Например, возьмем два блока Catch, один для ошибки при отсутствии прав доступа, а второй — для любых других ошибок, которые могут возникнуть в процессе выполнения скрипта:

try {
Get-Content -Path ″C:Files*″ -ErrorAction Stop
}
catch [System.UnauthorizedAccessException]
{
Write-Host ″File is not accessible.″
}
catch {
Write-Host ″Other type of error was found:″
Write-Host ″Exception type is $($_.Exception.GetType().Name)″
}
finally {
Write-Host ″Finish.″
}

обработка нескольких исключений в trycatch

Trap

Еще один способ обработки ошибок — это установка ловушки (trap). В этом варианте обработчик ошибок задается с помощью ключевого слова Trap, определяющего список команд, которые должны выполниться при возникновении прерывающей ошибки и исключения. Когда происходит исключение, то оболочка PowerShell ищет в коде инструкции Trap для данной ошибки, и если находит, то выполняет их.

Возьмем наиболее простой пример ловушки, которая обрабатывает все произошедшие исключения и выводит сообщение об ошибке:

trap {
Write-Host ″Error was found.″
}
Get-Content -Path C:File* -ErrorAction Stop

обработка исключения с помощью trap

Так же, как и для trycatch, ключевое слово Trap позволяет задавать тип исключения, указав его в квадратных скобках. К примеру, можно задать в скрипте несколько ловушек, одну нацелив на конкретную ошибку, а вторую на обработку оставшихся:

trap [System.Management.Automation.ItemNotFoundException]
{
Write-Host ″File is not accessible.″
break
}
trap {
Write-Host ″Other error was found.″
continue
}
Get-Content -Path C:File* -ErrorAction Stop

Вместе с Trap можно использовать ключевые слова Break и Continue, которые позволяют определить, должен ли скрипт продолжать выполняться после возникновения прерывающей ошибки. По умолчанию при возникновении исключения выполняются команды, входящие в блок Trap, выводится информация об ошибке, после чего выполнение скрипта продолжается со строки, вызвавшей ошибку:

trap {
Write-Host ″Error was found.″
}
Write-Host ″Before error.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error.″

обработка исключения с помощью trap без доп. параметров

Если использовать ключевое слово Break, то при возникновении ошибки будет выполнены команды в блоке Trap, после чего выполнение скрипта будет прервано:

trap {
Write-Host ″Error was found.″
break
}
Write-Host ″Before error.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error.″

Как  видно из примера, команда, следующая за исключением, не отработала и сообщение ″After error.″ выведено не было.

обработка исключения с помощью trap с параметром break

Если же в Trap включено ключевое слово Continue, то выполнение скрипта будет продолжено, так же как и в случае по умолчанию. Единственное отличие в том, что с Continue ошибка не записывается в поток Error и не выводится на экран.

trap {
Write-Host ″Error was found.″
continue
}
Write-Host ″Before error.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error.″

обработка исключения с помощью trap с параметром continue

Область действия

При отлове ошибок с помощью Trap надо помнить о такой вещи, как область действия (scope). Оболочка PowerShell является глобальной областью, в которую входят все процессы, запущенный скрипт получает собственную область, а если внутри скрипта определены функции, то внутри каждой определена своя, частная область действия. Это создает своего рода родительско-дочернюю иерархию.

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

Для примера возьмем функцию Content, внутри которой будет выполняться наш код. Область действия внутри функции назовем scope 1, а снаружи scope 2:

trap {
Write-Host ″Error was found.″
continue
}
function Content {
Write-Host ″Before error, scope1.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error, scope 1.″
}
Content
Write-Host ″After error, scope 2.″

При возникновении ошибки в функции Content оболочка будет искать ловушку внутри нее. Затем, не найдя ловушку внутри функции, оболочка выйдет из текущей области и будет искать в родительской области. Ловушка там есть, поэтому будет выполнена обработка ошибки, после чего Continue возобновит выполнение скрипта, но уже в родительской области (scope 2), не возвращаясь обратно в функцию (scope 1).

область действия, вариант 1

А теперь немного изменим скрипт, поместив ловушку внутрь функции:

function Content {
trap {
Write-Host ″Error was found.″
continue
}
Write-Host ″Before error, scope 1.″
Get-Content -Path C:File* -ErrorAction Stop
Write-Host ″After error, scope 1.″
}
Content
Write-Host ″After error, scope 2.″

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

область действия, вариант 2

Заключение

Если сравнить Try/Catch и Trap, то у каждого метода есть свои достоинства и недостатки. Конструкцию Try/Catch можно более точно нацелить на возможную ошибку, так как Catch обрабатывает только содержимое блока Try. Эту особенность удобно использовать при отладке скриптов, для поиска ошибок.

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

КРАТКОЕ ОПИСАНИЕ
    Описывает ключевое слово, обрабатывающее прерывающие ошибки.

ПОЛНОЕ ОПИСАНИЕ
    Прерывающая ошибка останавливает выполнение инструкции. Если
    Windows PowerShell не обрабатывает каким-либо образом прерывающую
    ошибку, Windows PowerShell прерывает и функцию или скрипт в
    текущем конвейере. В других языках, например C#, прерывающие
    ошибки называются исключениями.

    Ключевое слово Trap определяет список инструкций, которые нужно
    выполнить при возникновении прерывающей ошибки. Инструкции Trap
    обрабатывают прерывающие ошибки и обеспечивают продолжение
    выполнения скрипта или функции.

  Синтаксис

      Синтаксис инструкции Trap следующий:

          trap [[<тип_ошибки>]] {<список_инструкций>}

      Инструкция Trap включает список инструкций, которые нужно
      выполнить при возникновении прерывающей ошибки. Ключевое слово
      Trap может, кроме того, задавать тип ошибки. Тип ошибки
      указывается в квадратных скобках.

      Скрипт или команда могут содержать несколько инструкций Trap.
      Инструкции Trap можно помещать в любое место скрипта или команды.

  Перехват всех прерывающих ошибок

      При возникновении прерывающей ошибки, которая не обрабатывается
      скриптом или командой, Windows PowerShell проверяет, нет ли
      инструкции Trap для обработки этой ошибки. Если инструкция Trap
      есть, Windows PowerShell продолжает выполнять скрипт или
      команду в инструкции Trap.
 

      Ниже показан пример простейшей инструкции Trap:

          trap {«Обнаружена ошибка.»}

      Эта инструкция Trap перехватывает все прерывающие ошибки. Ниже
      показан пример функции, содержащей эту инструкцию Trap:

          function TrapTest {
              trap {«Обнаружена ошибка.»}
              nonsenseString
              }

      Эта функция включает бессмысленную строку, вызывающую ошибку.
      При выполнении эта функция возвращает следующие результаты:

          C:PS> TrapTest
          Обнаружена ошибка.

      Ниже показан пример инструкции Trap, выводящей ошибку с помощью
      автоматической переменной $_:

          function TrapTest {
              trap {«Обнаружена ошибка: $_»}
              nonsenseString
              }

      При выполнении эта версия функции возвращает следующие результаты:

          C:PS> TrapTest
          Обнаружена ошибка: условие «nonsenseString» не распознано
          как имя командлета, функции, файла скрипта или выполняемой
          программы. Проверьте правильность написания имени, наличие
          и правильность пути и повторите попытку.

      Инструкции Trap могут быть и сложнее. Инструкция Trap может
      включать несколько условий или вызовов функций. Она может
      записывать ошибку в журнал, тестировать или даже запускать
      другую программу.

   Перехват определенных прерывающих ошибок

      Ниже показан пример инструкции Trap, перехватывающей ошибки типа
      CommandNotFoundException:

          trap [System.Management.Automation.CommandNotFoundException]
              {«Перехвачена ошибка команды»}

      Когда функция или скрипт обнаруживает строку, не совпадающую с
      какой-нибудь известной командой, эта инструкция Trap отображает
      строку «Перехвачена ошибка команды». После выполнения
      инструкций списка Trap Windows PowerShell записывает объект
      ошибки в поток ошибок и продолжает выполнять скрипт.

      Windows PowerShell использует типы исключений Microsoft .NET
      Framework 3.0. В показанном ниже примере задается тип ошибок
      System.Exception:

          trap [System.Exception] {«Перехвачена ошибка»}

      Тип ошибок CommandNotFoundException наследуется от типа
      System.Exception. Эта инструкция перехватывает ошибку, которую
      создает неизвестная команда. Она, кроме того, перехватывает
      другие типы ошибок.

      В скрипте может быть несколько инструкций Trap. Одна ошибка
      может быть перехвачена только одной инструкцией Trap. Если
      происходит ошибка и при этом доступно несколько инструкций
      Trap, Windows PowerShell использует ту инструкцию Trap, тип
      ошибок которой лучше всего соответствует ошибке.

      В следующем примере скрипта содержится ошибка. Скрипт включает
      общую инструкцию Trap, перехватывающую любые прерывающие
      ошибки, и определенную инструкцию Trap, для которой задан тип
      CommandNotFoundException.

          trap {«Перехвачена другая прерывающая ошибка» } trap
          [System.Management.Automation.CommandNotFoundException] {«Перехвачена
   ошибка команды»} nonsenseString

      Выполнение скрипта приводит к таким результатам:

          Перехвачена ошибка команды
          Условие «nonsenseString» не распознано как имя командлета,
          функции, файла скрипта или выполняемой программы. Проверьте
          правильность написания имени, наличие и правильность пути и
          повторите попытку.
          В C:PS>testScript1.ps1:3 знак:19
          +   nonsenseString <<<<

      Так как Windows PowerShell не распознает «nonsenseString» как
      командлет или другой элемент, возвращается ошибка
      CommandNotFoundException. Эта прерывающая ошибка перехватывается
      определенной инструкцией Trap.

      Ниже показан пример скрипта с теми же инструкциями Trap, но с
      другой ошибкой:

          trap {«Перехвачена другая прерывающая ошибка» }
   trap [System.Management.Automation.CommandNotFoundException]
              {«Перехвачена ошибка команды»}
          1/$null

      Выполнение скрипта приводит к таким результатам:

          перехвачена другая прерывающая ошибка;
          попытка деления на ноль.
          В C:PS> errorX.ps1:3 знак:7
          +   1/ <<<< $null

      Попытка поделить на ноль не создает ошибку CommandNotFoundExcept
      ion. Эта ошибка перехватывается другой инструкцией Trap,
      предназначенной для любых прерывающих ошибок.

  Перехват ошибок и область

      Если прерывающая ошибка происходит в той же области, где
      находится инструкция Trap, Windows PowerShell после выполнения
      инструкций Trap продолжает с инструкции после ошибки. Если
      ошибка и инструкция Trap находятся в разных областях,
      выполнение продолжается со следующей инструкции из области, в
      которой находится инструкция Trap.
 

      Например, если ошибка происходит в функции, а инструкция Trap
      находится в этой функции, скрипт продолжается со следующей
      инструкции. Например, следующий скрипт содержит ошибку и
      инструкцию Trap:

          function function1 {
              trap { «Ошибка: » }
              NonsenseString
              «function1 выполнена»
              }

      Выполнение функции Function1 в следующей части скрипта приводит
      к таким результатам:

          function1
          Ошибка:
          Условие «NonsenseString» не распознано как имя командлета,
          функции, файла скрипта или выполняемой программы. Проверьте
          правильность написания имени, наличие и правильность пути и
          повторите попытку.
          В C:PS>TestScript1.ps1:3 знак:19
          +   NonsenseString <<<<

          function1 выполнена
 

      Инструкция Trap в функции перехватывает ошибку. После вывода
      сообщения Windows PowerShell продолжает выполнять функцию.
      Обратите внимание, что функция Function1 была выполнена.

      Сравните это со следующим примером, в котором содержатся те же
      ошибка и инструкция Trap. В этом примере инструкция Trap
      выполняется вне функции:

          function function2 {
              NonsenseString
              «function2 выполнена»
              }

          trap { «Ошибка: » }
              . . .
          function2

      Выполнение функции Function2 в следующей части скрипта приводит
      к таким результатам:

          Ошибка:
          Условие «NonsenseString» не распознано как имя командлета,
          функции, файла скрипта или выполняемой программы. Проверьте
          правильность написания имени, наличие и правильность пути и
          повторите попытку.
          В C:PS>TestScript2.ps1:4 знак:19
          +   NonsenseString <<<<

      В этом примере не была выполнена команда «function2 завершена».
      Несмотря на то что обе прерывающие ошибки произошли внутри
      функции, Windows PowerShell не возвращается обратно в функцию
      после выполнения инструкции Trap, потому что инструкция
      находилась вне функции.

  Использование ключевых слов Break и Continue

      Ключевые слова Break и Continue можно использовать в инструкции
      Trap, чтобы определить, будет ли скрипт или команда продолжать
      выполняться после прерывающей ошибки.

      Если в список инструкций Trap включена инструкция Break,
      Windows PowerShell прерывает функцию или скрипт. Следующий
      пример функции использует ключевое слово Break в инструкции Trap:

          C:PS> function break_example {
              trap {«Перехвачена ошибка»; break;}
              1/$null
              «Функция выполнена.»
              }

          C:PS> break_example
          Перехвачена ошибка
          Попытка деления на ноль.
          В строке:4 знак:7

      Инструкция Trap включала ключевое слово Break, поэтому функция
      перестала выполняться и строка «Функция выполнена» запущена не была.

      Если в инструкцию Trap включена инструкция Continue, Windows
      PowerShell продолжает после утверждения, вызвавшего ошибку, как
      если бы ключевых слов Break или Continue не было. Но с ключевым
      словом Continue Windows PowerShell не записывает ошибку в поток
      ошибок.

      Следующий пример функции использует ключевое слово Continue в
      инструкции Trap:

          C:PS> function continue_example {
              trap {«Перехвачена ошибка»; continue;}
       1/$null
              «Функция выполнена.»}

          C:PS> continue_example
          Перехвачена ошибка
          Функция выполнена.

      Функция продолжает выполнение после перехвата ошибки, и
      запускается инструкция «Функция выполнена». Ошибка не
      записывается в поток.

СМ. ТАКЖЕ
    about_Break
    about_Continue
    about_Throw
    about_Try_Catch_Finally
    about_Scopes
    about_Try_Catch_Finally

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

.

Инструкция Trap для обработки ошибок

Инструкция Trap для обработки ошибок появилась еще в PowerShell 1.0, но я ее по-прежнему часто использую в сценариях. Технология PowerShell построена на базе. NET Framework, и ошибки имеют несколько уровней. Возвращаемое PowerShell сообщение об ошибке верхнего уровня редко помогает решить проблему. Для примера рассмотрим случай использования метода SMO CheckTables(‘FAST’), по сути выполняющего инструкцию DBCC CHECKDB(N’AdventureWorks’, REPAIR_FAST). При вызове этого метода без блока обработки ошибок возвращается сообщение, как на рисунке 1.

Сообщение об ошибке, возвращаемое при использовании метода SMO CheckTables(‘FAST’)
Рисунок 1. Сообщение об ошибке, возвращаемое при использовании метода SMO CheckTables (‘FAST’)

Очевидно, что такое сообщение не несет никакой полезной информации, поэтому я часто использую функцию Trap:

# Handle any errors that occur
Trap {
# Handle the error
$err = $_.Exception
write-output $err.Message
while( $err.InnerException ) {
$err = $err.InnerExceptionне
write-output $err.Message
};
# End the script.
Break
}

Это позволяет при вызове метода CheckTables(‘FAST’) получить сообщение, подобное приведенному на рисунке 2.

Сообщения об ошибках, возвращаемые функцией Trap
Рисунок 2. Сообщения об ошибках, возвращаемые функцией Trap

Это сообщение содержит гораздо больше полезной информации и позволяет точно выяснить, в чем состоит проблема, и как ее решить. Ключевое слово break после точки с запятой в инструкции trap инициирует прекращение сценария после обнаружения и обработки ошибки. Ключевое слово continue позволяет продолжить выполнение сценария после обработки ошибок без прерывания.

Метод обработки ошибок Try-Catch-Finally

В PowerShell 2.0 был впервые реализован метод обработки ошибок Try-Catch-Finally, ставший привычным для большинства. NET-разработчиков. Этот метод отличается значительно большей гибкостью в обработке возможных проблем. Дополнительным преимуществом метода является возможность указывать различные типы обработки ошибок для разных ошибок. В приведенном в листинге примере также демонстрируется запуск метода CheckTables. Однако в данном случае каждый объект ItemNotFoundException обрабатывается отдельно, после чего обрабатываются все оставшиеся ошибки способом, аналогичным показанному в инструкции trap.

Очевидно, что такой вариант позволяет обрабатывать различные ошибки, а тот факт, что блоки Try-Catch-Finally могут быть вложенными, обеспечивает широкие возможности управления сценариями.

Листинг. Пример запуска метода CheckTables

try {
# Connect to the specified instance
$s = new-object ('Microsoft.SqlServer.Management.Smo.Server') $inst
$db = $s.Databases[$dbname]
$db.CheckTables('Fast')
}
catch [System.Management.Automation.ItemNotFoundException] {
write-output»$dbname database not found«
}
catch {
# Handle the error
$err = $_.Exception
write-output $err.Message
while( $err.InnerException ) {
$err = $err.InnerException
write-output $err.Message
}
}
finally {
write-output»script completed"
}

Try like this:

trap { write-host "file not found, skipping";continue;}
$modtime = Get-ItemProperty c:manoj -erroraction stop

Based on comments from OP:

I think you misunderstood what is being said in the article you have linked to:

In this example, we used continue to caused execution to return to the
scope the trap is in and execute the next command. It’s important to
note that execution only returns to the scope of the trap, so if the
exception was thrown inside a function, or even inside a if statement,
and trapped outside of it … the continue will pick up at the end of
the nested scope.

So if you do something like this:

trap{ write-host $_; continue;}
throw "blah"
write-host after

after will be printed.

But if you do something like this:

trap{ write-host $_ ; continue}
function fun($f) {


      throw "blah"
      write-host after
}

fun
write-host "outside after"

after will NOT be printed, but outside after will be.

Alternatively, use a try-catch block:

      try{
      $modtime = (Get-ItemProperty $f -erroraction stop).LastWriteTime
      write-host "if file not found then shouldn't see this"
      }
      catch{
        write-host "file not found, skipping".
      }

Error handling is just part of life when it comes to writing code. We can often check and validate conditions for expected behavior. When the unexpected happens, we turn to exception handling. You can easily handle exceptions generated by other people’s code or you can generate your own exceptions for others to handle.

Index

  • Index
  • Basic terminology
    • Exception
    • Throw and Catch
    • The call stack
    • Terminating and non-terminating errors
    • Swallowing an exception
  • Basic command syntax
    • Throw
      • Write-Error -ErrorAction Stop
      • Cmdlet -ErrorAction Stop
    • Try/Catch
    • Try/Finally
    • Try/Catch/Finally
  • $PSItem
    • PSItem.ToString()
    • $PSItem.InvocationInfo
    • $PSItem.ScriptStackTrace
    • $PSItem.Exception
      • $PSItem.Exception.Message
      • $PSItem.Exception.InnerException
      • $PSItem.Exception.StackTrace
  • Working with exceptions
    • Catching typed exceptions
    • Catch multiple types at once
    • Throwing typed exceptions
      • Write-Error -Exception
      • The big list of .Net exceptions
    • Exceptions are objects
    • Re-throwing an exception
      • Re-throwing a new exception
      • $PSCmdlet.ThrowTerminatingError()
  • Try can create terminating errors
    • $PSCmdlet.ThrowTerminatingError() inside try/catch
    • Public function templates
  • Trap
  • Closing remarks

Basic terminology

We need to cover some basic terms before we jump into this one.

Exception

An Exception is like an event that is created when normal error handling can not deal with the issue. Trying to divide a number by zero or running out of memory are examples of something that will create an exception. Sometimes the author of the code you are using will create exceptions for certain issues when they happen.

Throw and Catch

When an exception happens, we say that an exception is thrown. To handle a thrown exception, you need to catch it. If an exception is thrown and it is not caught by something, the script will stop executing.

The call stack

The call stack is the list of functions that have called each other. When a function is called, it gets added to the stack or the top of the list. When the function exits or returns, it will be removed from the stack.

When an exception is thrown, that call stack is checked in order for an exception handler to catch it.

Terminating and non-terminating errors

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.

Swallowing an exception

This is when you catch an error just to suppress it. Do this with caution because it can make troubleshooting issues very difficult.

Basic command syntax

Here is a quick overview of the basic exception handling syntax used in PowerShell.

Throw

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

Write-Error -ErrorAction Stop

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 Stop

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

Try/Catch

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.

Try/Finally

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.

Take a look at this example:

$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.

Because you are not catching the exception, it will still get propagated up the call stack.

Try/Catch/Finally

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

Now that we got the basics out of the way, we can dig a little deeper.

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( '\testnofilefound.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:blogthrowerror.ps1
Line                  :     Get-Resource
PositionMessage       : At C:blogthrowerror.ps1:5 char:5
                        +     Get-Resource
                        +     ~~~~~~~~~~~~
PSScriptRoot          : C:blog
PSCommandPath         : C:blogthrowerror.ps1
InvocationName        : Get-Resource

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

$PSItem.ScriptStackTrace

This property will show the order of function calls that got you to the code where the exception was generated.

PS:> $PSItem.ScriptStackTrace
at Get-Resource, C:blogthrowerror.ps1: line 13
at Do-Something, C:blogthrowerror.ps1: line 5
at <ScriptBlock>, C:blogthrowerror.ps1: line 18

I am only making calls to functions in the same script but this would track the calls if multiple scripts were involved.

$PSItem.Exception

This is the actual exception that was thrown.

$PSItem.Exception.Message

This is the general message that describes the exception and is a good starting point when troubleshooting. Most exceptions have a default message but can also be set to something custom when the exception is thrown.

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

Exceptions can contain inner exceptions. This is often the case when the code you are calling catches an exception and throws a different exception. They will place the original exception inside the new exception.

PS:> $PSItem.Exception.InnerExceptionMessage
The network path was not found.

I will revisit this later when I talk about re-throwing exceptions.

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

You will only get this stack trace when the event is thrown from managed code. I am calling a .Net framework function directly so that is all we can see in this example. Generally when you are looking at a stack trace, you are looking for where your code stops and the system calls begin.

Working with exceptions

There is more to exceptions than the basic syntax and exception properties.

Catching typed exceptions

You can be selective with the exceptions that you catch. Exceptions have a type and you can specify the type of exception you want to catch.

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.

Catch multiple types at once

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]"
}

Thank you /u/Sheppard_Ra for suggesting this addition.

Throwing typed exceptions

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

throw "Could not find: $path"

Use an exception accelerator like this:

throw [System.IO.FileNotFoundException] "Could not find: $path"

But you have to specify a message when you do it that way.

You can also create a new instance of an exception to be thrown. The message is optional when you do this because the system has default messages for all built in exceptions.

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

By using a typed exception, you (or others) can catch the exception by the type as mentioned in the previous section.

Write-Error -Exception

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

Then we can catch it like this:

catch [System.IO.FileNotFoundException]
{
    Write-Log $PSItem.ToString()
}

The big list of .Net exceptions

I compiled a master list with the help of the Reddit/r/PowerShell community that contains hundreds of .Net exceptions to complement this post.

  • The big list of .Net exceptions

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.

Exceptions are objects

If you start using a lot of typed exceptions, remember that they are objects. Different exceptions have different constructors and properties. If we look at the documentation 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
}

You will have to consult the .Net documentation for other constructors and object properties.

Re-throwing an exception

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.

There are times where you want to perform an action on an exception but re-throw the exception so something downstream can deal with it. We could write a message or log the problem close to where we discover it but handle the issue further up the stack.

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
}

We want to re-throw the exception to preserve the original execution information like source script and line number. If we throw a new exception at this point it will hide where the exception started.

Re-throwing a new exception

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 ThrowTerminatingError() 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 ThrowTerminatingError() 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
AssembliesMicrosoftFramework.NETPortablev4.6System.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 ThrowTerminatingError this way to re-throw.

catch
{
    $PSCmdlet.ThrowTerminatingError($PSItem)
}

This will change the source of the error to the Cmdlet and hide the internals of your function from the users of your Cmdlet.

Try can create terminating errors

Kirk Munro points out that some exceptions are only terminating errors when executed inside a try/catch block. Here is the example he gave me that generates a divide by zero runtime exception.

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

Then invoke it like this to see it generate the error and still output the message.

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

But by placing that same code inside a try/catch, we see something else happen.

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

We see the error become a terminating error and not output the first message. What I don’t like about this one is that you can have this code in a function and it will act differently if someone is using a try/catch.

I have not ran into issues with this myself but it is corner case to be aware of.

$PSCmdlet.ThrowTerminatingError() inside try/catch

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{...}.

Public function templates

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.

Trap

I focused on the try/catch aspect of exceptions. But there is one legacy feature I need to mention before we wrap this up.

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

I personally never adopted this approach but I can see the value in admin or controller scripts that will log any and all exceptions, then still continues to execute.

Adding proper exception handling to your scripts will not only make them more stable, but it will also make it easier for you to troubleshoot those exceptions.

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 ThrowTerminatingError my goto exception handler for every funciton.

Понравилась статья? Поделить с друзьями:
  • Transport fever 2 ошибка при запуске
  • Transport fever 2 ошибка an error just occurred
  • Transmission ошибка input output error
  • Transmission service requi 01 ошибка вольво
  • Transmission remote keenetic ошибка подключения