Обработка ошибок и повторные попытки в PowerShell — от ловушки нерабочего try/catch до exit code и типовых схем повтора

· · PowerShell, Windows, Обработка ошибок, Повторные попытки, Автоматизация, Улучшение эксплуатации, Скрипт, Планировщик заданий

«Ночной пакетный процесс завершился ошибкой, но в Планировщике заданий значилось “успешно” (0x0), и никто этого не заметил». «Написал try/catch, а выполнение в catch не попадает». «Из-за кратковременного сбоя сети падает раз в месяц». Стоит только вывести скрипт PowerShell в эксплуатацию, как подобные жалобы приходят неизбежно. Между скриптом, который радует вас при запуске вручную, и скриптом, который безостановочно работает по ночам без присмотра, стоит стена — проектирование обработки ошибок и повторного выполнения (retry).

Сложность в том, что модель ошибок PowerShell тонко отличается от модели исключений, привычной по большинству языков программирования. «Ошибка есть, а обработка продолжается» и «вроде бы поймал в catch, а оно проскочило мимо» в большинстве случаев — не баг, а поведение PowerShell строго по спецификации; если писать код, не зная этого механизма, штампуются скрипты, которые проглатывают сбой и завершаются как ни в чём не бывало.

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

1. Сначала вывод

  • Ошибки PowerShell делятся на «незавершающие» и «завершающие» (завершающие на уровне инструкции / завершающие скрипт). Незавершающая ошибка выводит сообщение и позволяет конвейеру продолжиться, по умолчанию не попадая в try/catch.1
  • Стандартное решение — добавлять -ErrorAction Stop к любой команде, которую должен перехватить try/catch. Stop повышает статус незавершающей ошибки до завершающей, и её может обработать catch. Есть и альтернатива — задать $ErrorActionPreference (по умолчанию Continue) в значение Stop в начале скрипта.12
  • -ErrorAction переопределяет $ErrorActionPreference только для одной конкретной команды. При этом они не полностью симметричны: -ErrorAction управляет только незавершающими ошибками.1
  • Сбой нативной команды (robocopy, git, внешний EXE) по умолчанию не становится ошибкой PowerShell. Ненулевой код завершения устанавливает $? в $false и попадает в $LASTEXITCODE, но ErrorRecord при этом не создаётся и в catch ничего не попадает. Успех определяйте по $LASTEXITCODE.1
  • В PowerShell 7.4 параметр $PSNativeCommandUseErrorActionPreference стал штатной функцией. При $true ненулевой код завершения порождает незавершающую ошибку, и в сочетании с $ErrorActionPreference = 'Stop' её можно поймать через try/catch (значение по умолчанию — $false).32
  • Внутри catch $_ — это ErrorRecord. $_.Exception даёт сам объект исключения, а для повышенной ошибки через $_.Exception.ErrorRecord можно добраться до исходной информации об ошибке. Блок catch с указанным типом исключения позволяет отдельно обрабатывать только ожидаемые ошибки.14
  • Об успехе или неудаче обязательно сообщайте вовне через exit code. Код завершения задаётся ключевым словом exit; при запуске через pwsh -File / powershell.exe -File это значение становится кодом завершения процесса. Без exit нормальное завершение даёт 0, а необработанное исключение — 1.56
  • Три принципа повторных попыток: только временные ошибки, верхняя граница, идемпотентность. Не маскируйте retry бизнес-ошибки, увеличивайте интервал по схеме экспоненциальной задержки и проектируйте обработку так, чтобы повторный запуск не приводил к двойной обработке. Только при выполнении всех трёх условий скрипт становится «безопасным для повторного запуска».

2. Два вида ошибок — почему try/catch не срабатывает

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

На практике ловушкой оказываются именно незавершающие ошибки. Когда командлет вроде Get-Content или Get-ChildItem не может обработать конкретный элемент входных данных, он обычно выдаёт именно незавершающую ошибку: красное сообщение об ошибке появляется, но обработка продолжается и не попадает ни в try/catch, ни в trap.1

# [Ловушка] catch не сработает ни разу, и будет выведено даже "Готово"
try {
    Get-Content -Path 'C:\Data\несуществующий-файл.txt'   # незавершающая ошибка
    Write-Host 'Готово'                                    # выполнится, несмотря на ошибку строкой выше
}
catch {
    Write-Host 'Сюда не попадём'
}

# [Стандартное решение] -ErrorAction Stop повышает ошибку до завершающей, чтобы её обработал catch
try {
    Get-Content -Path 'C:\Data\несуществующий-файл.txt' -ErrorAction Stop
    Write-Host 'Готово'                                    # пропускается при ошибке
}
catch {
    Write-Host "Поймано: $($_.Exception.Message)"
}

Когда действует -ErrorAction Stop или $ErrorActionPreference = 'Stop', движок оборачивает незавершающую ошибку в ActionPreferenceStopException и повышает её до завершающей. Именно эта повышенная ошибка и доходит до catch внутри блока try — таков точный механизм.1 А вот исключения методов .NET (например, [int]::Parse('abc')) и ошибки разрешения имени команды с самого начала являются завершающими ошибками, поэтому попадают в catch без каких-либо дополнительных мер.1

Идея «а не поставить ли $ErrorActionPreference = 'Stop' всегда» наполовину верна. Для скрипта, работающего без присмотра, безопаснее остановиться и сообщить о сбое, чем проглотить ошибку и двигаться дальше, поэтому установка Stop в начале — хороший вариант по умолчанию. Но учтите, что $ErrorActionPreference действует на текущую область видимости и на дочерние, а значит меняет поведение вызываемых модулей и функций, а для операций уборки, которым допустимо завершаться неудачей (например, удаление временных файлов), нужно отдельно возвращать -ErrorAction SilentlyContinue.2

3. Что читать внутри catch — как устроен обход ErrorRecord

В блоке catch переменная $_ содержит ErrorRecord. Именно отсюда берётся вся информация, которую стоит фиксировать в журнале.14

try {
    Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
    # Catch с указанным типом обрабатывает отдельно только "ожидаемые сбои".
    # Даже для повышенной ошибки движок сопоставляет её с исходным типом исключения
    Write-Warning "Ошибка ввода-вывода: $($_.Exception.Message)"
}
catch {
    # Неожиданное логируем вместе с контекстом и пробрасываем дальше (не проглатываем)
    $rec = $_   # $_ - это ErrorRecord
    Write-Warning ('Тип: {0} / Расположение: {1} / Объект: {2}' -f `
        $rec.Exception.GetType().FullName,
        $rec.InvocationInfo.PositionMessage,
        $rec.TargetObject)
    throw       # throw без аргументов пробрасывает ту же ошибку выше
}
finally {
    # finally выполняется и при успехе, и при ошибке, и при остановке через Ctrl+C. Уборка - сюда
    if ($tempFile -and (Test-Path $tempFile)) { Remove-Item $tempFile -ErrorAction SilentlyContinue }
}

Здесь стоит обратить внимание на три момента.

  • $_.Exception — это сам объект исключения. Ошибка, повышенная через -ErrorAction Stop, оборачивается в ActionPreferenceStopException, но при сопоставлении типов в catch движок смотрит на исходный тип исключения (например, ItemNotFoundException), поэтому catch с указанием типа можно писать как обычно. До исходного ErrorRecord можно добраться через $_.Exception.ErrorRecord.1
  • $_.InvocationInfo.PositionMessage содержит «в каком файле, в какой строке, какая команда» — и от того, есть ли эта информация в журнале при выполнении без присмотра, время расследования отличается на порядок.
  • Блок finally выполняется независимо от того, завершился ли try успешно, с ошибкой или был остановлен через Ctrl+C. Уборку вроде закрытия соединений или удаления временных файлов размещайте именно в finally.7

Вопрос о том, на каком уровне перехватывать ошибку и где вести журнал, — общий для всех языков. Принципы, сформулированные в статье «Где размещать catch и логирование при обработке исключений» (перехватывать на границе, не проглатывать ошибки, избегать двойного логирования), применимы к PowerShell без каких-либо изменений.

4. Успех нативных команд — $?, $LASTEXITCODE и новая функция 7.4

Ещё одна большая брешь — это нативные команды: robocopy, git, внутренние EXE-файлы компании. Внешние программы не участвуют в системе ошибок PowerShell и сообщают о сбое через код завершения. Поведение по умолчанию таково.1

Событие Поведение (по умолчанию)
Ненулевой код завершения $? становится $false, код завершения попадает в $LASTEXITCODE
Создание ErrorRecord Не происходит (не добавляется даже в $Error)
try/catch Не срабатывает

Иначе говоря, try { robocopy ... } catch { ... } (по умолчанию) вообще ничего не перехватывает. Успех нативной команды нужно проверять через $LASTEXITCODE. $? — это булево значение «успешна ли предыдущая операция», и для нативных команд оно становится $true только при коде завершения 0.1 Отметим также, что в Windows PowerShell 5.1 $? иногда становился $false просто из-за того, что нативная команда что-то писала в stderr, а в PowerShell 7 это изменили: теперь $false устанавливается только при ненулевом коде завершения. Это изменение приближено к реальности — запись в stderr сама по себе не считается сбоем.8

# Успех нативной команды определяем по $LASTEXITCODE
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR /R:2 /W:5
if ($LASTEXITCODE -ge 8) {
    # У robocopy коды 0-7 - это успех (информация о наличии копирования и т. п.), 8 и выше - сбой
    throw "Сбой robocopy (ExitCode=$LASTEXITCODE)"
}

Начиная с PowerShell 7.4 доступен параметр $PSNativeCommandUseErrorActionPreference, меняющий это поведение. Он был добавлен как экспериментальная функция в 7.3 и стал штатной функцией (mainstream) в 7.4.3 При $true нативная команда с ненулевым кодом завершения порождает незавершающую ошибку с явным указанием кода завершения, и эта ошибка подчиняется $ErrorActionPreference. То есть в сочетании со Stop сбой внешней команды тоже попадает в try/catch.12

# PowerShell 7.4+: сбой внешних команд тоже обрабатывается через try/catch (по умолчанию $false)
$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'

try {
    git.exe fetch origin
}
catch {
    Write-Warning "Сбой git: $($_.Exception.Message)"
    throw
}

& {
    # Для команд вроде robocopy, где ненулевой код не равен сбою, временно отключаем
    # параметр внутри блока скрипта и определяем успех по $LASTEXITCODE как раньше
    # (после выхода из блока значение восстанавливается)
    $PSNativeCommandUseErrorActionPreference = $false
    robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR
    if ($LASTEXITCODE -ge 8) { throw "Сбой robocopy (ExitCode=$LASTEXITCODE)" }
}

Пример с robocopy взят прямо из официальной документации не просто так: есть команды, использующие ненулевой код завершения как нормальную информацию, поэтому при включении параметра массово нужно продумывать зоны-исключения.2 В окружениях, где доступна только Windows PowerShell 5.1, этой функции попросту нет, так что там стоит унифицированно определять успех через $LASTEXITCODE. Разница поведения между 5.1 и 7 — частая ловушка при миграции, поэтому стоит также заглянуть в статью «Различия и переход между Windows PowerShell 5.1 и PowerShell 7».

5. Проектирование exit code — чтобы Планировщик заданий и мониторинг могли определить успех

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

  • exit <число> явно задаёт код завершения скрипта. exit также устанавливает значение $LASTEXITCODE.59
  • При запуске через pwsh -File (powershell.exe -File) значение, переданное в exit, становится кодом завершения процесса как есть. Без оператора exit нормальное завершение даёт 0, а завершение из-за необработанного исключения — 1.56
  • При запуске скрипта через -Command код завершения вроде exit 10 внутри скрипта не сохраняется. Он округляется до 0 или 1 в зависимости от успеха последней команды (если же exit 10 написан прямо в самой командной строке, возвращается именно это значение). Если эксплуатация опирается на разные коды завершения скрипта, стандартный подход — запуск через -File.6

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

# Invoke-NightlyExport.ps1 - каркас, позволяющий Планировщику заданий определить успех
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'   # Для выполнения без присмотра по умолчанию делаем "остановиться и сообщить"

# Ведём полную стенограмму выполнения, включая stdout и ошибки, как журнал (-Append дописывает в дневной файл)
Start-Transcript -Path "C:\Logs\NightlyExport_$(Get-Date -Format yyyyMMdd).log" -Append

try {
    Export-DailyData      # Основная бизнес-логика (вызывает функцию из модуля)
    exit 0                # Явно сигнализируем об успехе
}
catch [System.Net.WebException] {
    Write-Warning "Ошибка связи: $($_.Exception.Message)"
    exit 10               # Временная ошибка - оставляем задаче возможность настроить повтор
}
catch {
    Write-Warning "Непредвиденная ошибка: $($_.Exception.Message)"
    Write-Warning $_.InvocationInfo.PositionMessage
    exit 1                # Постоянная ошибка - без повтора, требует внимания человека
}
finally {
    Stop-Transcript       # В finally стенограмма закрывается даже при выходе через exit
}

Start-Transcript — это командлет, который целиком записывает в текст весь ввод и вывод сессии, позволяя воспроизвести «что было на экране в тот момент» без необходимости вручную городить echo или перенаправления.10 Его стоит использовать не вместо, а вместе с собственными функциями логирования — как последний рубеж обороны. Проектирование журналов и борьба с их разрастанием разобраны в статье «Прикладной PowerShell — безопасная автоматизация анализа логов, архивирования и отчётности».

Секрет в том, чтобы не переусложнять назначение exit code. Достаточно детализации вроде: 0 = успех, 1 = постоянная ошибка (нужно смотреть человеку), диапазон 10-х = временная ошибка (можно повторить), — и на этом можно прямо строить определение успеха в «Последнем результате выполнения» Планировщика заданий или в инструментах управления заданиями. Настройки на стороне задачи (повтор при сбое, способ проверки результата выполнения) разобраны в статье «Задачи Планировщика заданий не запускаются или завершаются с 0x1 — разбор причин и безопасное проектирование эксплуатации».

6. Проектирование повторных попыток — различаем временные и бизнес-ошибки

Наконец, повторное выполнение. Ценность retry в том, чтобы автоматически поглощать временные ошибки и не будить людей посреди ночи, но если внедрить его небрежно, возникают уже другие проблемы: бесконечные повторы постоянного сбоя или порча данных из-за двойной обработки. Принципов три.

  • Повторять только временные ошибки. Ограничивайтесь сбоями, которые может разрешить время: кратковременный обрыв сети, временная блокировка файла, ожидание запуска зависимой службы. Некорректный ввод, нехватку прав и ошибки настройки нужно сразу же проваливать, передавая информацию человеку через exit code и журнал.
  • Спроектировать верхнюю границу и интервал. Определите предельное число попыток, а интервал увеличивайте по схеме экспоненциальной задержки (2 секунды, 4, 8…). Долбить с фиксированным интервалом собеседника, который и так испытывает сбой, только мешает его восстановлению.
  • Обеспечить идемпотентность (безопасность повторного запуска). И retry, и повторный запуск из Планировщика заданий означают одно: «та же самая обработка выполняется ещё раз». Это предполагает такие приёмы, как публикация результата через временный файл с последующим переименованием или запись обработанных ID для отсечения повторного импорта.

В виде шаблона это сводится к следующему.

function Invoke-WithRetry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [scriptblock] $Operation,
        # Значение 0 или меньше привело бы к нормальному завершению без единого запуска, поэтому требуем не менее 1
        [ValidateRange(1, 100)]
        [int] $MaxAttempts = 4,
        # Отрицательное значение вызовет отдельную ошибку в Start-Sleep при повторе, поэтому отсекаем уже при связывании параметра
        [ValidateRange(0, 3600)]
        [int] $BaseDelaySeconds = 2,
        # Перечисляем только те типы исключений, которые имеет смысл повторять (по умолчанию - сеть и I/O)
        [Type[]] $RetryableExceptions = @([System.IO.IOException], [System.Net.WebException])
    )
    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        try {
            # Сначала принимаем вывод в переменную и возвращаем только после успеха. Если делать
            # return & $Operation напрямую, при исключении после частичного вывода этот частичный
            # результат утечёт к вызывающему коду, и при успешном повторе те же данные придут дважды
            $output = & $Operation
            return $output
        }
        catch {
            $ex = $_.Exception
            $isRetryable = $RetryableExceptions | Where-Object { $ex -is $_ }
            if (-not $isRetryable -or $attempt -eq $MaxAttempts) {
                throw   # Бизнес-ошибка либо исчерпан лимит попыток - завершаем неудачей как есть
            }
            # Ограничиваем сверху экспоненциально растущее время ожидания (чтобы конфигурация
            # с большим числом попыток не ждала слишком долго и не вышла за допустимый диапазон Start-Sleep)
            $delay = [math]::Min($BaseDelaySeconds * [math]::Pow(2, $attempt - 1), 300)
            Write-Warning "Сбой (попытка ${attempt}): $($ex.Message) - повтор через ${delay} сек."
            Start-Sleep -Seconds $delay
        }
    }
}

# Использование: обрабатываемая операция должна быть переведена в завершающую ошибку через -ErrorAction Stop
Invoke-WithRetry -Operation {
    Copy-Item -Path '\\fileserver\out\daily.csv' -Destination 'D:\Work' -ErrorAction Stop
}

# Замечание при повторе Invoke-RestMethod / Invoke-WebRequest в PowerShell 7:
# в версии 7 сбой связи приходит не как WebException времён 5.1, а как исключение
# семейства HttpRequestException, поэтому с настройками по умолчанию повтор не сработает.
# Кроме того, постоянная HTTP-ошибка вроде 404 приходит тем же типом, поэтому, получив
# ответ, нужно самостоятельно решать по коду статуса, стоит ли его повторять
Invoke-WithRetry -RetryableExceptions ([System.Net.Http.HttpRequestException]) -Operation {
    # -SkipHttpErrorCheck принимает даже ответ с ошибкой без исключения, чтобы решить по коду, как поступить
    $r = Invoke-WebRequest -Uri 'https://api.example.co.jp/orders' -TimeoutSec 30 -SkipHttpErrorCheck
    if ($r.StatusCode -in 408, 429, 500, 502, 503, 504) {
        # Бросаем как HttpRequestException только временные коды -> будет повторено
        throw [System.Net.Http.HttpRequestException]::new("Временная ошибка HTTP: $($r.StatusCode)")
    }
    if ($r.StatusCode -ge 400) {
        throw "Постоянная ошибка HTTP: $($r.StatusCode)"   # другой тип, поэтому не повторяется
    }
    $r.Content | ConvertFrom-Json
}

Ключевой момент — объекты retry выбираются явно, по типу исключения. Если написать «повторять всё, что попало в catch», то даже постоянная ошибка вроде неверного параметра будет впустую пробовать четыре раза и ждать между попытками. После начала эксплуатации реалистичный путь развития — постепенно добавлять в $RetryableExceptions типы временных ошибок, реально замеченные в логах. Подобные общие функции стоит выносить в модуль для повторного использования (см. «Проектирование аргументов и модуляризация PowerShell»). Кроме того, логика retry и ветвления по ошибкам — это именно то место, где стоит писать тесты Pester («Настройка тестирования PowerShell через Pester — практические приёмы, снижающие риск поломки эксплуатационных скриптов»).

7. Практические рекомендации (таблица решений)

Вопрос Варианты Ориентир для решения
Поведение при ошибке по умолчанию Оставить Continue / задать $ErrorActionPreference = ‘Stop’ в начале Для выполнения без присмотра безопаснее «остановиться и сообщить». Интерактивный скрипт для расследования может оставаться на Continue2
Место, которое нужно перехватить Надеяться на лучшее / явно указать -ErrorAction Stop Командлеты чаще всего выдают незавершающие ошибки. Указывайте Stop явно на строке, которую хотите перехватить1
Успех нативной команды Не проверять / определять по $LASTEXITCODE / $PSNativeCommandUseErrorActionPreference из 7.4 В окружениях со смешанным 5.1 унифицируйте на $LASTEXITCODE. Только для 7.4+ - новая функция плюс зоны-исключения для robocopy и подобных32
Сообщение об успехе вовне Только журнал / проектировать exit code и запускать через -File Журнал - для людей, exit code - для машин, нужны оба. Запуск через -Command “сплющивает” код завершения6
Стенограмма выполнения Только собственный журнал / дополнительно Start-Transcript Страховка, сохраняющая и то, что не попадает в собственный журнал (например, стандартный вывод внешних команд)10
Повторные попытки Повторять все ошибки / только временные + экспоненциальная задержка + идемпотентность Повтор бизнес-ошибки - прямой путь к аварии. Нужен комплект из верхней границы, интервала и идемпотентности

8. Итоги

  • Ошибки PowerShell делятся на незавершающие и завершающие, и незавершающие по умолчанию не попадают в try/catch. Стандартное решение — явно указывать -ErrorAction Stop для команд, которые нужно перехватить.
  • По умолчанию $ErrorActionPreference равен Continue. Для скрипта, работающего без присмотра, задайте Stop в начале — это структурно предотвращает аварию, при которой сбой проглатывается и скрипт завершается как ни в чём не бывало.
  • Сбой нативной команды по умолчанию не попадает в catch. Определяйте его через $LASTEXITCODE или, начиная с PowerShell 7.4, задействуйте $PSNativeCommandUseErrorActionPreference.
  • В catch фиксируйте в журнале тип исключения, сообщение и позицию из $_ (ErrorRecord), а уборку размещайте в finally. finally выполняется даже при Ctrl+C или exit.
  • Сообщайте об успехе или сбое вовне через exit code. При запуске через -File значение exit становится кодом завершения как есть, позволяя Планировщику заданий или мониторингу определить успех.
  • Повторные попытки стройте по трём принципам: только временные ошибки, ограниченная сверху экспоненциальная задержка и идемпотентность. Постоянные ошибки должны сразу проваливаться и передаваться человеку.

Похожие статьи

Смежные области консультирования

KomuraSoft LLC (合同会社小村ソフト) занимается ревью проектирования обработки ошибок и повторных попыток для ночных пакетных процессов и типовых скриптов, расследованием прерывистых сбоев вроде «сбой, а отображается как успех» или «падает лишь раз в месяц», а также повышением эксплуатационного качества существующих скриптовых активов.

Справочные ссылки

  1. Microsoft Learn, about_Error_Handling. О трёх категориях ошибок (незавершающая, завершающая на уровне инструкции, завершающая скрипт), о том, что незавершающая ошибка по умолчанию не попадает в catch/trap, о механизме повышения через -ErrorAction Stop (ActionPreferenceStopException и $_.Exception.ErrorRecord), о том, что catch с указанным типом сопоставляется с исходным типом исключения, о семантике $? и $LASTEXITCODE, о том, что ненулевой код завершения нативной команды по умолчанию не создаёт ErrorRecord, и о поведении $PSNativeCommandUseErrorActionPreference.  2 3 4 5 6 7 8 9 10 11 12 13 14 15

  2. Microsoft Learn, about_Preference_Variables. О том, что $ErrorActionPreference по умолчанию равен Continue, что параметр -ErrorAction имеет приоритет для отдельной команды, что настройка применяется к текущей области видимости и дочерним, что $PSNativeCommandUseErrorActionPreference по умолчанию равен $false, и о примере временного отключения параметра внутри блока скрипта для команд вроде robocopy, использующих ненулевой код завершения как информацию.  2 3 4 5 6 7

  3. Microsoft Learn, What’s New in PowerShell 7.4. О том, что экспериментальная функция PSNativeCommandErrorActionPreference ($PSNativeCommandUseErrorActionPreference) стала штатной (mainstream) в PowerShell 7.4.  2 3

  4. Microsoft Learn, Everything you wanted to know about exceptions. О доступе к информации об исключении через $_ внутри блока catch, о том, что команда с -ErrorAction Stop и ошибка Write-Error становятся обрабатываемыми в catch, и о паттерне освобождения ресурсов через try/finally.  2

  5. Microsoft Learn, about_Language_Keywords. О том, что ключевое слово exit задаёт код завершения и отражается в $LASTEXITCODE, что скрипт, запущенный через pwsh -File, возвращает числовой аргумент exit как код завершения, и что при отсутствии оператора exit нормальное завершение даёт 0, а необработанное исключение — 1.  2 3

  6. Microsoft Learn, about_Pwsh. О том, как определяется код завершения при запуске через -File, и о том, что запуск через -Command преобразует любой код завершения, кроме 0 и 1, в 1, поэтому для сохранения кода завершения нужен exit $LASTEXITCODE.  2 3 4

  7. Microsoft Learn, about_Try_Catch_Finally. О синтаксисе try/catch/finally, о блоках catch с указанным типом и множественных catch, а также о том, что блок finally выполняется при успехе, при ошибке, при остановке через Ctrl+C и даже при exit внутри catch. 

  8. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. О том, что в PowerShell 7 изменили поведение: $? больше не становится $false только из-за записи нативной команды в stderr, а только при ненулевом коде завершения. 

  9. Microsoft Learn, about_Automatic_Variables. О том, что $LASTEXITCODE хранит код завершения нативной программы или скрипта, и что при вызове через pwsh -File устанавливается 1 при завершении из-за исключения, значение ключевого слова exit или 0 при нормальном завершении. 

  10. Microsoft Learn, Start-Transcript. О записи команд сессии и консольного вывода в текстовый файл, о дозаписи через -Append, о расположении и имени файла по умолчанию и об остановке через Stop-Transcript.  2

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

Задачи планировщика заданий Windows не запускаются или завершаются с 0x1 — диагностика причин и проектирование надёжного выполнения по расписанию

Разбираем принципы проектирования надёжного выполнения задач по расписанию в Windows: учётные записи выполнения и типы входа в систему, т...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Я написал try/catch в PowerShell, но выполнение почему-то не попадает в catch. Почему?
Потому что большинство ошибок, которые выдают командлеты, — незавершающие (non-terminating). try/catch перехватывает только завершающие ошибки: незавершающая ошибка выводит сообщение и позволяет обработке продолжиться, не попадая ни в catch, ни в trap. Стандартное решение — добавить -ErrorAction Stop к команде, которую нужно перехватить (или задать $ErrorActionPreference = 'Stop' в начале скрипта). Это повышает статус незавершающей ошибки до завершающей, и try/catch начинает её обрабатывать.
Как правильно выбирать между $? и $LASTEXITCODE?
$? — это булево значение, показывающее, успешно ли завершилась предыдущая операция; оно устанавливается и для командлетов, и для нативных команд. $LASTEXITCODE — это код завершения последней выполненной нативной программы (или скрипта, вызвавшего exit), и он не меняется при ошибках командлетов. Для определения успеха внешних команд вроде robocopy или git надёжнее ориентироваться именно на $LASTEXITCODE, поскольку по нему можно проверить и точный смысл кода завершения. Учтите, что ненулевой код завершения нативной команды по умолчанию не попадает в catch.
Как настроить определение успеха или сбоя скрипта PowerShell из Планировщика заданий?
В конце скрипта (и в блоках catch) явно задавайте код завершения ключевым словом exit, а задачу запускайте через pwsh -File (или powershell.exe -File), отслеживая значение «Последний результат выполнения». При запуске через -File значение, переданное в exit, становится кодом завершения процесса как есть; без exit нормальное завершение даёт 0, а необработанное исключение — 1. При запуске через -Command любой код завершения, кроме 0 и 1, преобразуется в 1, поэтому если эксплуатация строится на кодах завершения, стандартный подход — запуск через -File.
Для каких ошибок стоит делать повторные попытки?
Только для временных ошибок, при которых повтор действительно может изменить результат: кратковременный сбой сети, временная блокировка файла, ожидание запуска зависимой службы и тому подобное. Бизнес-ошибки и постоянные ошибки — некорректные входные данные, нехватка прав, неверные настройки — при повторе будут завершаться неудачей раз за разом, поэтому их не нужно повторять: пусть скрипт сразу завершается ошибкой и сообщает об этом человеку через журнал и exit code. Даже при повторных попытках нужно задавать верхнюю границу по количеству и интервалу, увеличивать интервал по схеме экспоненциальной задержки, а обработку заранее проектировать идемпотентной, чтобы повторный запуск не приводил к двойной обработке.
Что делает параметр $PSNativeCommandUseErrorActionPreference в PowerShell 7.4?
Это настройка, которая заставляет генерировать ошибку PowerShell (незавершающую) при завершении нативной команды с ненулевым кодом. Она была добавлена как экспериментальная функция в PowerShell 7.3 и стала штатной в 7.4 (значение по умолчанию — $false). При $true поведение подчиняется $ErrorActionPreference, поэтому в сочетании со Stop сбой внешней команды можно перехватывать через try/catch. Однако есть команды вроде robocopy, которые используют ненулевой код завершения как обычную информацию, поэтому на таких участках нужно временно возвращать значение $false.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог