PowerShell 的錯誤處理與重新執行設計 ── 從 try/catch 失效的陷阱到 exit code、重試的實務定石

· · PowerShell, Windows, 錯誤處理, 重試, 自動化, 維運改善, 腳本, 工作排程器

「夜間批次處理明明失敗了,工作排程器上卻顯示成功(0x0),沒有人發現」「明明寫了 try/catch,卻沒有進入 catch」「網路瞬斷導致每個月只掛掉一次」── 只要把 PowerShell 腳本投入實際維運,這類諮詢就必定會出現。在手邊執行、跑得順就滿足的腳本,與無人值守、每晚持續運作的腳本之間,橫著一道錯誤處理與重新執行(重試)設計的高牆。

麻煩的是,PowerShell 的錯誤模型與一般程式語言的例外模型有著微妙的差異。「明明出現了錯誤,處理卻繼續執行下去」「明明應該被 catch 住,卻悄悄溜走了」,這些情況大多不是臭蟲,而是 PowerShell 規格上本來就如此運作,如果不了解機制就直接動手寫,就會大量生產出「把失敗吞掉、卻正常結束的腳本」。

本文以用 PowerShell 將公司內部例行作業自動化的資訊系統負責人與開發者為對象,從終止錯誤與非終止錯誤的區別開始,整理原生指令的成敗判定、能讓工作排程器與監控系統判斷成敗的 exit code 設計,以及能承受暫時性錯誤的重試模式,並附上官方文件作為佐證。

1. 先講結論

  • PowerShell 的錯誤分為「非終止錯誤」與「終止錯誤(陳述式終止/腳本終止)」。非終止錯誤只會顯示訊息並讓管線繼續執行,在預設情況下不會進入 try/catch1
  • 由於計算方式有兩種,先在這裡整理清楚。官方文件的分類是「非終止/陳述式終止/腳本終止」這 3 個類別,這是以引擎「要停到什麼範圍」(僅管線/僅該陳述式/整個呼叫堆疊)為軸線的分法。1 另一方面,撰寫者最先想知道的是「會不會進入 try/catch」,以這個軸線來看則分為非終止錯誤與終止錯誤兩種。本文主要採用兩分類的軸線,並在必要處觸及 3 個類別的細節。
  • 想用 try/catch 捕捉的指令,加上 -ErrorAction Stop 是實務上的定石。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
  • 重試的三大原則是:限定於暫時性錯誤、設有上限、冪等(即使同一處理執行多次,結果也不會改變的性質,詳見第 6 章)。不用重試來蒙混業務錯誤、以指數退避拉大間隔、將設計做成即使重新執行也不會造成重複處理。這三點都齊備,才稱得上是「可以重新執行的腳本」。

2. 兩種錯誤 ── 為什麼 try/catch 不起作用

PowerShell 的錯誤,首先分為非終止錯誤終止錯誤兩種。而終止錯誤又分為陳述式終止錯誤腳本終止錯誤,因此細分下去共有 3 個類別。非終止錯誤不會停止管線、只會回報;陳述式終止錯誤只停止該陳述式,接著進行下一個陳述式;腳本終止錯誤則會回捲整個呼叫堆疊。1

在實務上容易踩到的陷阱是非終止錯誤。Get-ContentGet-ChildItem 這類 Cmdlet 在處理個別輸入失敗時,發出的多半是非終止錯誤,明明會顯示紅色的錯誤訊息,處理卻照樣繼續,既不會進入 try/catch,也不會進入 trap1

# 【陷阱】永遠不會進入 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 包裝起來,升級為終止錯誤。在 try 區塊內,正是這個被升級的錯誤傳到 catch,這才是正確的機制。1

另一方面,也有一開始就是終止錯誤(=什麼都不用做就會進入 catch)的情況。陳述式終止錯誤正是如此,官方文件列舉了以下發生來源。1

  • 呼叫了不存在的指令時(CommandNotFoundException)
  • 參數繫結失敗時(ParameterBindingException。例如傳入無法轉換成數值參數的字串等)
  • .NET 的方法擲回例外時(如 [int]::Parse('abc'))
  • Cmdlet 或進階函式透過 $PSCmdlet.ThrowTerminatingError() 回報「這次呼叫已經無法繼續」時

正如其名,它只會停止「該陳述式」,因此腳本會從下一個陳述式繼續執行。1

# 陳述式終止錯誤:這一行會停止,但下一行仍會執行
[int]::Parse('abc')
Write-Output '這一行會被執行'

# 因為是終止錯誤,即使不加 -ErrorAction Stop 也會進入 catch
try   { [int]::Parse('abc') }
catch { Write-Warning "捕捉到: $($_.Exception.Message)" }

麻煩的是,即使是「檔案不存在」這種相同的情況,依 Cmdlet 內部實作的不同,有時是非終止錯誤,有時是陳述式終止錯誤。要撰寫者每次都去分辨是不切實際的,因此實務上的解答,是在想要捕捉的那一行明確加上 -ErrorAction Stop,統一成不論哪一種都能確實傳到 catch 的形式。

「那麼一律把 $ErrorActionPreference = 'Stop' 設起來不就好了嗎」這個想法算是對了一半。在無人執行的腳本中,與其吞下錯誤繼續前進,不如停下來回報失敗來得安全,因此在開頭設為 Stop 是不錯的預設值。不過要意識到,$ErrorActionPreference 會作用於該作用域與子作用域,因此連呼叫的模組或函式的行為都會跟著改變;而對於失敗也無妨的收尾處理(如刪除暫存檔),則需要個別重新加上 -ErrorAction SilentlyContinue2

3. 在 catch 中該讀取什麼 ── ErrorRecord 的走訪方式

catch 區塊中的 $_ 裝的是 ErrorRecord。應該留存在日誌中的資訊,都可以從這裡取得。14

try {
    Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
    # 用指定型別的 catch,只個別處理「預期內的失敗」。
    # 即使是被升級的錯誤,引擎也會用原本的例外型別來比對
    Write-Warning "I/O 錯誤: $($_.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 }
}

有 3 個重點。

  • $_.Exception 就是例外本體。以 -ErrorAction Stop 升級的錯誤會被包裝在 ActionPreferenceStopException 中,但在 catch 的型別比對時,引擎會看的是原本的例外型別(例如 ItemNotFoundException),因此指定型別的 catch 可以照原樣寫。原始的 ErrorRecord 可以透過 $_.Exception.ErrorRecord 追溯。1
  • $_.InvocationInfo.PositionMessage 裡裝著「是哪個檔案的第幾行、哪個指令」,在無人執行的日誌中,有沒有這項資訊,調查所需的時間會相差一個數量級。
  • finally 區塊無論 try 成功、發生錯誤,或是被 Ctrl+C 中止,都會執行。關閉連線、刪除暫存檔之類的收尾處理,應該放在 finally 裡。7

在哪一層 catch、在哪裡寫日誌,這個設計上的討論是跨語言共通的。在「應該在哪裡 catch 例外並輸出日誌、進行錯誤處理 - 以實務向整理呼叫階層的邊界與職責」中整理的原則(在邊界處 catch、不要吞掉錯誤、避免重複記錄日誌),同樣可以直接套用在 PowerShell 上。

4. 原生指令的成敗 ── $? 與 $LASTEXITCODE,以及 7.4 的新功能

另一個大陷阱,是 robocopy、git,以及公司內部 EXE 這類原生指令。外部程式並不參與 PowerShell 的錯誤系統,而是用結束代碼來回報失敗。預設的行為如下。1

事件 行為(預設)
非零結束代碼 $? 變成 $false,結束代碼存入 $LASTEXITCODE
產生 ErrorRecord 不會(也不會被加入 $Error)
try/catch 不會進入

也就是說,try { robocopy ... } catch { ... }(在預設情況下)什麼都捕捉不到。原生指令的成敗判定,要用 $LASTEXITCODE 來寫。$? 是「前一個操作是否成功」的布林值,對原生指令而言,只有結束代碼為 0 時才會是 $true1 另外,在 Windows PowerShell 5.1 中,原生指令只要寫入了 stderr,$? 有時就會變成 $false;而 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 中以實驗性功能新增,並在 7.4 成為正式功能(mainstream)3 設為 $true 時,結束代碼非零的原生指令會引發標明結束代碼的非終止錯誤,並依循 $ErrorActionPreference。也就是說,只要與 Stop 搭配,外部指令的失敗也能被 try/catch 接住。12

在 5.1 與 7 混用的環境中,請務必先確認版本前提。這項功能只有 PowerShell 7.4 以後才能使用,7.3 中它還是實驗性功能(功能名稱為 PSNativeCommandErrorActionPreference),需要用 Enable-ExperimentalFeature 啟用並重新啟動工作階段才能生效。3 而 Windows PowerShell 5.1 根本不存在這個變數,即使賦值 $true 也不會發生任何事(只是單純建立了一個新變數,也不會出現錯誤,所以很難察覺)。如果同一支腳本有可能同時在 5.1 與 7 上執行,就不要依賴這項功能,包括後面幾章在內,統一以 $LASTEXITCODE 判定會比較安全。

# 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 同時也會把該值設定到 $LASTEXITCODE59
  • 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'   # 在無人執行中,把「停止並回報」設為預設值

# 把包含標準輸出、錯誤的執行證跡留存為日誌(用 -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 是把工作階段的輸入與輸出整個記錄成文字檔的 Cmdlet,即使不安排 echo 或重新導向,也能重現「當時畫面上顯示了什麼」。10 它與自訂的日誌函式並不互斥,作為最後一道防線一併使用是有價值的。日誌的設計與避免肥大化的對策,在「PowerShell 腳本應用 ── 安全地自動化日誌調查、封存與報表化」中有討論。

exit code 的分配訣竅是不要弄得太複雜。0=成功、1=恆久性錯誤(交由人查看)、10 幾號=暫時性錯誤(可以重新執行),這種粒度就已經足夠,可以直接對應工作排程器的「上次執行結果」或工作管理工具的成敗判定。工作排程器那一側的設定(失敗時重新執行、確認執行結果的方式)請參考「工作排程器的工作不執行、以 0x1 結束 ── 原因排查與安全的維運設計」。

6. 重試設計 ── 區分暫時性錯誤與業務錯誤

最後是重新執行。重試的價值在於「自動吸收暫時性錯誤,不在半夜把人叫醒」,但若隨便加進去,就會產生「對恆久性失敗沒完沒了地重試」「因重複處理而破壞資料」這類另一種事故。原則有三個。

  • 只對暫時性錯誤重試。限定於網路瞬斷、檔案暫時被鎖定、等待相依服務啟動等,時間可以解決的失敗。輸入不正確、權限不足、設定錯誤則要立即失敗,透過 exit code 與日誌交給人處理。
  • 設計上限與間隔。決定次數上限,間隔則以指數退避(2 秒、4 秒、8 秒……)逐漸拉大。對正處於故障中的對象以固定間隔持續敲打,只會妨礙它復原。
  • 要做到冪等(重新執行也安全)。無論是重試,還是工作排程器的重新執行,意味的都是「同一項處理會再跑一次」。這需要以「輸出先寫入暫存檔再重新命名」「記錄已處理過的 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
}

# 重試 PowerShell 7 的 Invoke-RestMethod / Invoke-WebRequest 時的注意事項:
# 在 7 中,通訊失敗不再是 5.1 時代的 WebException,而是以 HttpRequestException 系傳達,
# 因此在預設狀態下不會被重試。另外,像 404 這種恆久性的 HTTP 錯誤回應也會以同一種型別
# 傳達,因此在收到回應時,要自行依狀態碼判斷「是否值得重試」
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
}

重點在於,用例外的型別明確選定重試對象。如果寫成「只要 catch 到就一律重試」,就連參數寫錯這種恆久性錯誤,也會白白嘗試 4 次、浪費等待時間。實際上線之後,把在真實日誌中觀察到的暫時性錯誤型別逐步加進 $RetryableExceptions,是比較務實的養成方式。

另外,由於 Invoke-WithRetry 本體篇幅較長,請不要每次使用時都貼到腳本裡,而是整個存成 Retry.psm1 這樣的檔案,用 Import-Module 載入。這樣可以避免每次複製貼上時,只有 catch 裡混進舊版本這類事故(模組化的做法請參考「PowerShell 腳本的參數設計與模組化 ── 從「能運作的腳本」到「可以交給別人的腳本」」)。此外,重試與錯誤分支的邏輯,正是值得用 Pester 撰寫測試的部分(參見「用 Pester 整備 PowerShell 測試 ── 讓維運腳本不易損壞的實務做法」)。

7. 實務定石(判斷表)

論點 選項 判斷依據
錯誤的預設行為 維持 Continue / 在開頭將 $ErrorActionPreference 設為 ‘Stop’ 無人執行以「停止並回報」較安全。用於互動式調查的腳本則維持 Continue 即可2
想要 catch 的地方 祈禱 / 明確加上 -ErrorAction Stop Cmdlet 大多是非終止錯誤。想捕捉的行要明確加上 Stop1
原生指令的成敗 放著不管 / 以 $LASTEXITCODE 判定 / 7.4 的 $PSNativeCommandUseErrorActionPreference 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 的值會直接成為結束代碼,讓工作排程器與監控系統能判斷成敗。
  • 重試要遵守限定暫時性錯誤、上限指數退避、冪等這三大原則。恆久性錯誤則要立即失敗、交給人處理。

相關文章

相關諮詢領域

合同會社小村軟體提供夜間批次處理、例行作業腳本的錯誤處理與重試設計審查,針對「明明失敗了卻被當成成功」「每個月只出一次問題」這類間歇性故障的調查,以及既有腳本資產的維運品質改善。

參考連結

  1. Microsoft Learn,about_Error_Handling。說明非終止錯誤、陳述式終止錯誤、腳本終止錯誤這 3 種分類,非終止錯誤在預設情況下不會進入 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 16 17

  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)已在 PowerShell 7.4 成為正式功能(mainstream)。  2 3 4

  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 中止或 catch 內執行 exit 時也會執行。 

  8. Microsoft Learn,Differences between Windows PowerShell 5.1 and PowerShell 7.x。說明在 PowerShell 7 中,原生指令僅寫入 stderr 並不會使 $? 變成 $false,已改為只有非零結束代碼時才會變成 $false 這項變更。 

  9. Microsoft Learn,about_Automatic_Variables。說明 $LASTEXITCODE 會保存原生程式或腳本的結束代碼,以及以 pwsh -File 呼叫時,因例外結束會設為 1、正常完成會設為 0,或設為 exit 關鍵字所指定的值。 

  10. Microsoft Learn,Start-Transcript。說明把工作階段的指令與主控台輸出記錄到文字檔、以 -Append 附加寫入、預設的儲存位置與檔名,以及以 Stop-Transcript 停止記錄。  2

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

為什麼在 PowerShell 中寫了 try/catch,卻沒有進入 catch?
因為 Cmdlet 所發出的錯誤大多是非終止錯誤(non-terminating error)。try/catch 只會捕捉終止錯誤,非終止錯誤只會顯示訊息並繼續執行,不會進入 catch。實務上的定石做法,是在想要捕捉的指令加上 -ErrorAction Stop(或是在腳本開頭將 $ErrorActionPreference 設為 'Stop')。這樣一來,非終止錯誤就會被升級為終止錯誤,變得可以用 try/catch 處理。
$? 和 $LASTEXITCODE 該怎麼區分使用?
$? 是表示前一個操作是否成功的布林值,無論是 Cmdlet 還是原生指令都會設定它。$LASTEXITCODE 則是最後執行的原生程式(或已 exit 的腳本)的結束代碼,Cmdlet 發生錯誤時並不會改變它。要判定 robocopy、git 等外部指令的成敗時,用能確認結束代碼實際意義的 $LASTEXITCODE 來判斷較為確實。請注意,原生指令的非零結束代碼在預設情況下不會進入 catch。
要如何讓工作排程器判斷 PowerShell 腳本的成敗?
在腳本結尾(以及 catch 區塊)用 exit 關鍵字明確指定結束代碼,工作排程器那一側則以 pwsh -File(或 powershell.exe -File)啟動,監控「上次執行結果」的數值。以 -File 啟動時,exit 所指定的值會直接成為處理程序的結束代碼;若沒有 exit,正常結束為 0,發生未處理例外則為 1。若以 -Command 啟動,0 與 1 以外的結束代碼都會被轉換成 1,因此若要以結束代碼建立維運機制,用 -File 啟動才是實務定石。
重試應該針對哪些錯誤進行?
應限定於重新嘗試就可能改變結果的暫時性錯誤(網路瞬斷、檔案暫時被鎖定、等待服務啟動等)。輸入資料錯誤、權限不足、設定錯誤等業務錯誤、恆久性錯誤,無論重試幾次都會失敗,因此不應重試,而要立即失敗,透過日誌與 exit code 通知人員。即使要重試,也必須為次數與間隔設定上限,以指數退避拉大間隔,並且前提是要把處理設計成冪等,讓重新執行也不會造成重複處理。
PowerShell 7.4 的 $PSNativeCommandUseErrorActionPreference 是做什麼用的設定?
這是在原生指令以非零結束代碼結束時,觸發 PowerShell 錯誤(非終止錯誤)的設定。它在 PowerShell 7.3 中以實驗性功能新增,並於 7.4 成為正式功能(預設為 $false)。設為 $true 時會依循 $ErrorActionPreference,因此若與 Stop 搭配使用,就能以 try/catch 捕捉外部指令的失敗。不過,像 robocopy 這類會把非零結束代碼當作正常資訊使用的指令也存在,因此需要留意在那個區段內暫時改回 $false 等處理。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽