PowerShell Error Handling and Retry Design — From the try/catch Trap to Exit Codes and Retry Best Practices
· Go Komura · PowerShell, Windows, Error Handling, Retry, Automation, Operational Improvement, Script, Task Scheduler
“The nightly batch job had failed, but Task Scheduler still showed success (0x0), and nobody noticed” — “I wrote a try/catch, but execution never reaches the catch block” — “it drops just once a month because of a brief network outage.” The moment you put a PowerShell script into production, complaints like these always show up. There’s a wall standing between a script that satisfies you when you run it by hand and one that runs unattended every night: the design of error handling and retries.
The tricky part is that PowerShell’s error model differs subtly from the exception model you’re used to in most programming languages. “An error was raised, but processing kept going” and “I thought I’d caught it, but it slipped right through” are, in most cases, not bugs — they’re PowerShell behaving exactly as specified. Write code without understanding the mechanism, and you end up mass-producing scripts that swallow failures and exit as if everything succeeded.
This article is aimed at in-house IT staff and developers who automate routine internal work with PowerShell. Backed by the official documentation, it walks through the distinction between terminating and non-terminating errors, judging whether a native command succeeded, exit code design that lets Task Scheduler and monitoring tools tell success from failure, and a retry pattern that survives transient errors.
1. The Bottom Line First
- PowerShell errors fall into “non-terminating errors” and “terminating errors” (statement-terminating or script-terminating). A non-terminating error displays a message and lets the pipeline continue, and by default it never reaches try/catch.1
- The standard fix is to add
-ErrorAction Stopto any command you want try/catch to catch. Stop promotes a non-terminating error into a terminating one so catch can handle it. You can also set$ErrorActionPreference(default: Continue) to Stop at the top of the script.12 -ErrorActionoverrides$ErrorActionPreferencefor that one command. The two aren’t perfectly symmetric, though —-ErrorActiononly controls non-terminating errors.1- A failure in a native command (robocopy, git, an external EXE) doesn’t, by default, become a PowerShell error. A nonzero exit code sets
$?to$falseand lands in$LASTEXITCODE, but no ErrorRecord gets created and it never reaches catch. Judge success or failure with$LASTEXITCODE.1 - In PowerShell 7.4,
$PSNativeCommandUseErrorActionPreferencebecame a mainstream feature. Set it to$trueand a nonzero exit code raises a non-terminating error; combine that with$ErrorActionPreference = 'Stop'and try/catch can catch it (the default is$false).32 - Inside catch,
$_is the ErrorRecord.$_.Exceptiongives you the exception itself, and for a promoted error,$_.Exception.ErrorRecordtakes you back to the original error information. A catch block that specifies an exception type lets you handle only the errors you expected, individually.14 - Always report success or failure to the outside world through the exit code. Set the exit code with the
exitkeyword, and if the script is launched withpwsh -File/powershell.exe -File, that value becomes the process’s exit code. Without exit, a normal completion is 0 and an unhandled exception is 1.56 - Retries follow three principles: limited to transient errors, capped, and idempotent. Don’t paper over business errors with retries; widen the interval with exponential backoff; and design the processing so re-running it never causes double processing. Only once all three are in place do you have a script that’s actually safe to re-run.
2. Two Kinds of Errors — Why try/catch Doesn’t Catch Everything
PowerShell errors fall into three categories: non-terminating errors (report without stopping the pipeline), statement-terminating errors (stop only that statement and move on to the next one), and script-terminating errors (unwind the entire call stack).1
In practice, the trap is the non-terminating error. When a cmdlet like Get-Content or Get-ChildItem fails to process one particular input, what it raises is typically a non-terminating error — a red error message shows up, but processing keeps going, and it never reaches either try/catch or trap.1
# [Trap] catch never runs, and "Done" gets printed anyway
try {
Get-Content -Path 'C:\Data\does-not-exist.txt' # non-terminating error
Write-Host 'Done' # runs even though the line above failed
}
catch {
Write-Host 'This never runs'
}
# [Standard fix] -ErrorAction Stop promotes it to a terminating error so catch can handle it
try {
Get-Content -Path 'C:\Data\does-not-exist.txt' -ErrorAction Stop
Write-Host 'Done' # skipped when the line above errors
}
catch {
Write-Host "Caught: $($_.Exception.Message)"
}
When -ErrorAction Stop or $ErrorActionPreference = 'Stop' is in effect, the engine wraps the non-terminating error in an ActionPreferenceStopException and promotes it to a terminating error. Inside a try block, it’s this promoted error that arrives at catch — that’s the precise mechanism at work.1 By contrast, exceptions from .NET methods (like [int]::Parse('abc')) and command-name resolution failures are terminating errors from the start, so they reach catch without any extra effort.1
The idea of “so why not just always set $ErrorActionPreference = 'Stop'” is half right. For an unattended script, stopping and reporting the failure is safer than swallowing the error and pressing on, so setting Stop at the top is a good default. Keep in mind, though, that $ErrorActionPreference applies to that scope and its child scopes, which changes the behavior of any module or function you call, and that cleanup steps where failure is acceptable (deleting temp files, say) need -ErrorAction SilentlyContinue added back explicitly.2
3. What to Read Inside catch — Walking Through the ErrorRecord
Inside a catch block, $_ holds the ErrorRecord. Everything worth logging comes from here.14
try {
Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
# A type-specific catch handles only "expected failures" individually.
# Even for a promoted error, the engine matches against the original exception type
Write-Warning "I/O error: $($_.Exception.Message)"
}
catch {
# Log unexpected errors with full context and rethrow (don't swallow them)
$rec = $_ # $_ is the ErrorRecord
Write-Warning ('Type: {0} / Location: {1} / Target: {2}' -f `
$rec.Exception.GetType().FullName,
$rec.InvocationInfo.PositionMessage,
$rec.TargetObject)
throw # A bare throw (no argument) propagates the same error upward
}
finally {
# finally runs whether try succeeds, errors out, or gets stopped with Ctrl+C. Put cleanup here
if ($tempFile -and (Test-Path $tempFile)) { Remove-Item $tempFile -ErrorAction SilentlyContinue }
}
There are three things worth understanding here.
$_.Exceptionis the exception itself. An error promoted by-ErrorAction Stopgets wrapped in anActionPreferenceStopException, but when catch does its type matching, the engine looks at the original exception type (say,ItemNotFoundException), so you can write a type-specific catch exactly as you would normally. You can trace back to the original ErrorRecord via$_.Exception.ErrorRecord.1$_.InvocationInfo.PositionMessagetells you exactly which file, which line, and which command — and whether your unattended-run logs include this is the difference between an investigation that takes minutes and one that takes hours.- The finally block runs whether try succeeds, errors out, or gets stopped with Ctrl+C. Cleanup like closing connections or deleting temp files belongs in finally.7
The design question of at which layer to catch and where to log is universal across languages. The principles laid out in Where Should catch and Logging Go in Exception Handling? — catch at the boundary, don’t swallow errors, avoid double-logging — apply just as directly to PowerShell.
4. Native Command Success or Failure — $?, $LASTEXITCODE, and the New 7.4 Feature
The other big gap is native commands — robocopy, git, in-house EXEs. External programs don’t participate in PowerShell’s error system; they report failure through an exit code instead. Here’s the default behavior.1
| Event | Behavior (default) |
|---|---|
| Nonzero exit code | $? becomes $false, and the exit code lands in $LASTEXITCODE |
| ErrorRecord creation | None (not even added to $Error) |
| try/catch | Not triggered |
In other words, try { robocopy ... } catch { ... } catches nothing (by default). Write your success/failure check for a native command against $LASTEXITCODE. $? is a Boolean for “did the immediately preceding operation succeed,” and for a native command it’s only $true when the exit code is 0.1 Note also that in Windows PowerShell 5.1, a native command merely writing to stderr could set $? to $false, but PowerShell 7 changed this so $? only becomes $false on a nonzero exit code — a change that better matches reality, since writing to stderr isn’t the same as failing.8
# Judge a native command's outcome with $LASTEXITCODE
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR /R:2 /W:5
if ($LASTEXITCODE -ge 8) {
# robocopy treats 0-7 as success-ish (informational, e.g. whether anything was copied); 8+ means failure
throw "robocopy failed (ExitCode=$LASTEXITCODE)"
}
Starting in PowerShell 7.4, you can change this behavior with $PSNativeCommandUseErrorActionPreference. It was added as an experimental feature in 7.3 and became a mainstream feature in 7.4.3 Set it to $true, and a native command with a nonzero exit code raises a non-terminating error that spells out the exit code, and that error follows $ErrorActionPreference. In other words, combine it with Stop, and external-command failures ride along with try/catch too.12
# PowerShell 7.4+: handle external-command failures with try/catch too (default is $false)
$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'
try {
git.exe fetch origin
}
catch {
Write-Warning "git failed: $($_.Exception.Message)"
throw
}
& {
# For a command like robocopy, where nonzero doesn't mean failure, disable this
# temporarily inside a script block and fall back to judging by $LASTEXITCODE as before
# (it reverts once you exit the block)
$PSNativeCommandUseErrorActionPreference = $false
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR
if ($LASTEXITCODE -ge 8) { throw "robocopy failed (ExitCode=$LASTEXITCODE)" }
}
Just as the official documentation shows for robocopy itself, some commands use a nonzero exit code as normal, meaningful information, so if you turn this on broadly, you need to design exception zones around them.2 In environments stuck on Windows PowerShell 5.1 only, this feature doesn’t exist at all, so standardize on judging by $LASTEXITCODE. Behavioral differences between 5.1 and 7 are exactly the kind of thing that trips people up during a migration, so it’s worth documenting them carefully whenever you plan one.
5. Exit Code Design — Making Success or Failure Visible to Task Scheduler and Monitoring
Once you’ve caught the error, the next step is reporting it to the outside world. In practice, the only way Task Scheduler or a monitoring tool has of knowing whether a script succeeded is the process’s exit code. Let’s pin down the exact specification.
exit <number>lets you set the script’s exit code explicitly.exitalso sets a value into$LASTEXITCODE.59- When launched with
pwsh -File(powershell.exe -File), whatever value you pass to exit becomes the process’s exit code, unchanged. Without an exit statement, a normal completion is 0, and terminating on an unhandled exception is 1.56 - Launch the script with
-Commandinstead, and an exit code likeexit 10inside the script doesn’t survive. It gets rounded down to 0 or 1 based on whether the last command succeeded (though if you writeexit 10directly in the command string itself, that value is returned as-is). If your operations depend on distinguishing between different script exit codes, launching with-Fileis the standard approach.6
Put this specification into a skeleton, and an unattended script template looks like this.
# Invoke-NightlyExport.ps1 — a skeleton that lets Task Scheduler judge success or failure
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop' # For unattended runs, make 'stop and report' the default
# Keep a full transcript, including stdout and errors, as a log (append to a daily file with -Append)
Start-Transcript -Path "C:\Logs\NightlyExport_$(Get-Date -Format yyyyMMdd).log" -Append
try {
Export-DailyData # The actual business logic (calls a function from a module)
exit 0 # Explicitly signal success
}
catch [System.Net.WebException] {
Write-Warning "Communication error: $($_.Exception.Message)"
exit 10 # Transient-error family — leaves room for the task to be configured to retry
}
catch {
Write-Warning "Unexpected error: $($_.Exception.Message)"
Write-Warning $_.InvocationInfo.PositionMessage
exit 1 # Permanent error — don't retry; a human needs to look at this
}
finally {
Stop-Transcript # In finally, the transcript gets closed even when we leave via exit
}
Start-Transcript is a cmdlet that records the entire input and output of a session as text, letting you reproduce exactly what was on screen at the time without wiring up echo statements or redirects yourself.10 It’s worth running alongside your own logging functions rather than treating them as mutually exclusive — think of it as a last line of defense. Log design and dealing with log growth are covered in Applied PowerShell Scripting — Safely Automating Log Investigation, Archiving, and Reporting.
The trick with exit-code assignment is not to overengineer it. A granularity of roughly 0 = success, 1 = permanent error (a human needs to look), and the 10s = transient error (safe to retry) is more than enough, and it maps directly onto Task Scheduler’s “Last Run Result” or a job-management tool’s success/failure check. For the task-side configuration (retrying on failure, how to check the result), see When Task Scheduler Tasks Don’t Run or Exit with 0x1 — Isolating the Cause and Designing for Reliable Operation.
6. Retry Design — Distinguishing Transient Errors from Business Errors
Finally, retries. The value of a retry is that it absorbs transient errors automatically and doesn’t wake someone up in the middle of the night — but bolt one on carelessly, and you create a different set of accidents: endlessly retrying a permanent failure, or corrupting data through double processing. There are three principles.
- Retry only transient errors. Limit retries to failures that time itself can resolve — a network blip, a temporary file lock, waiting for a dependent service to start. Let invalid input, insufficient permissions, and misconfiguration fail immediately, and hand them off to a human via the exit code and the log.
- Design an upper bound on count and interval. Set a maximum retry count, and widen the interval with exponential backoff (2 seconds, 4 seconds, 8 seconds, and so on). Hammering a struggling counterpart at a fixed interval only gets in the way of its recovery.
- Make it idempotent (safe to re-run). Both a retry and a Task Scheduler re-run mean “the same processing runs again.” The underlying assumption has to be a design like publishing output via a temp file plus rename, or recording processed IDs to reject duplicate ingestion.
As a pattern, this converges on the following shape.
function Invoke-WithRetry {
[CmdletBinding()]
param(
[Parameter(Mandatory)] [scriptblock] $Operation,
# Passing 0 or less would let the loop exit successfully without ever running once, so force it to be at least 1
[ValidateRange(1, 100)]
[int] $MaxAttempts = 4,
# A negative value would cause a different error in Start-Sleep during a retry, so reject it right at parameter binding
[ValidateRange(0, 3600)]
[int] $BaseDelaySeconds = 2,
# Enumerate only the exception types worth retrying (the default is I/O and networking)
[Type[]] $RetryableExceptions = @([System.IO.IOException], [System.Net.WebException])
)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
try {
# Capture the output in a variable first and return it only after success. Returning & $Operation
# directly would let partial output leak to the caller if an exception occurs partway through,
# so a later successful retry would deliver the same data twice
$output = & $Operation
return $output
}
catch {
$ex = $_.Exception
$isRetryable = $RetryableExceptions | Where-Object { $ex -is $_ }
if (-not $isRetryable -or $attempt -eq $MaxAttempts) {
throw # A business error, or we've hit the retry limit — let it fail as-is
}
# Cap the exponentially growing wait time (so a config with many attempts doesn't wait
# too long, and doesn't exceed what Start-Sleep will accept)
$delay = [math]::Min($BaseDelaySeconds * [math]::Pow(2, $attempt - 1), 300)
Write-Warning "Failed (attempt ${attempt}): $($ex.Message) — retrying in ${delay} seconds"
Start-Sleep -Seconds $delay
}
}
}
# Usage: make sure the target operation is turned into a terminating error with -ErrorAction Stop
Invoke-WithRetry -Operation {
Copy-Item -Path '\\fileserver\out\daily.csv' -Destination 'D:\Work' -ErrorAction Stop
}
# A caveat when retrying Invoke-RestMethod / Invoke-WebRequest on PowerShell 7:
# on 7, a communication failure arrives as an HttpRequestException-family type rather than
# the WebException from the 5.1 era, so it won't be retried with the defaults above. Also,
# a permanent HTTP error response like 404 arrives as the same type, so once a response
# comes back, you need to sort out "worth retrying" yourself, based on the status code
Invoke-WithRetry -RetryableExceptions ([System.Net.Http.HttpRequestException]) -Operation {
# -SkipHttpErrorCheck receives even an error response without throwing, so we can inspect the code and decide how to throw
$r = Invoke-WebRequest -Uri 'https://api.example.co.jp/orders' -TimeoutSec 30 -SkipHttpErrorCheck
if ($r.StatusCode -in 408, 429, 500, 502, 503, 504) {
# Throw only the transient codes as an HttpRequestException → gets retried
throw [System.Net.Http.HttpRequestException]::new("Transient HTTP error: $($r.StatusCode)")
}
if ($r.StatusCode -ge 400) {
throw "Permanent HTTP error: $($r.StatusCode)" # different type, so it's not retried
}
$r.Content | ConvertFrom-Json
}
The key point is that which exceptions get retried is chosen explicitly, by type. Write “retry everything that gets caught,” and you end up waiting through four pointless attempts even for a permanent error like a bad parameter. Once it’s in production, a realistic way to grow this is to add whatever transient-error types you actually observe in the logs to $RetryableExceptions. It’s worth factoring a shared helper like this out into a reusable module. Retry logic and error branching are also exactly the kind of thing worth writing Pester tests for (see Testing PowerShell with Pester — A Practical Approach to Making Operations Scripts Harder to Break).
7. Practical Rules of Thumb (Decision Table)
| Question | Options | Rule of thumb |
|---|---|---|
| Default error behavior | Leave it Continue / Set $ErrorActionPreference = ‘Stop’ at the top | Unattended runs are safer “stopping and reporting.” An interactive investigation script can stay on Continue2 |
| Where you want to catch | Hope for the best / Add -ErrorAction Stop explicitly | Cmdlets mostly raise non-terminating errors. Add Stop explicitly to any line you want to catch1 |
| Native command success/failure | Ignore it / Judge by $LASTEXITCODE / 7.4’s $PSNativeCommandUseErrorActionPreference | In environments with a mix of 5.1, standardize on $LASTEXITCODE. If everything is 7.4+, use the new feature plus exception zones for things like robocopy32 |
| Reporting success/failure externally | Logs only / Design exit codes and launch with -File | Logs are for humans, exit codes are for machines — you need both. Launching with -Command flattens the exit code6 |
| Execution transcript | Your own logging only / Also use Start-Transcript | A safety net that captures output your own logging can’t (e.g., an external command’s stdout)10 |
| Retries | Retry every error / Limit to transient errors + exponential backoff + idempotency | Retrying a business error is an accident waiting to happen. Combine a cap, an interval, and idempotency |
8. Summary
- PowerShell errors split into non-terminating and terminating errors, and non-terminating errors don’t reach try/catch by default. The standard fix is to add
-ErrorAction Stopexplicitly to any command you want to catch. $ErrorActionPreferencedefaults to Continue. Set an unattended script’s$ErrorActionPreferenceto Stop at the top to structurally prevent the accident of swallowing a failure and exiting as if it succeeded.- A native command’s failure doesn’t reach catch by default. Judge it with
$LASTEXITCODE, or, on PowerShell 7.4 and later, put$PSNativeCommandUseErrorActionPreferenceto work. - Inside catch, log the exception’s type, message, and location from
$_(the ErrorRecord), and put cleanup in finally. finally runs even on Ctrl+C or an exit. - Report success or failure to the outside world through the exit code. Launched with
-File, the value passed toexitbecomes the process’s exit code as-is, letting Task Scheduler or monitoring tell success from failure. - Retries follow three principles: limited to transient errors, capped exponential backoff, and idempotency. Let permanent errors fail immediately and hand them to a human.
Related Articles
- Applied PowerShell Scripting — Safely Automating Log Investigation, Archiving, and Reporting
- Testing PowerShell with Pester — A Practical Approach to Making Operations Scripts Harder to Break
- When Task Scheduler Tasks Don’t Run or Exit with 0x1 — Isolating the Cause and Designing for Reliable Operation
- Where Should catch and Logging Go in Exception Handling?
- PowerShell Execution Policy and Script Signing
Related Consulting Areas
Komura Software LLC handles reviewing error-handling and retry design for nightly batch jobs and routine scripts, investigating intermittent failures like “reported as success even though it actually failed” or “drops just once a month,” and improving the operational quality of existing script assets.
- Technical Consulting & Design Review
- Bug Investigation & Root Cause Analysis
- Legacy Asset Reuse & Migration Support
- Contact
References
-
Microsoft Learn, about_Error_Handling. On the three categories of errors (non-terminating, statement-terminating, script-terminating), how a non-terminating error doesn’t reach catch/trap by default, the promotion mechanism via -ErrorAction Stop (ActionPreferenceStopException and $_.Exception.ErrorRecord), how a type-specific catch matches against the original exception type, the semantics of $? and $LASTEXITCODE, how a native command’s nonzero exit code doesn’t generate an ErrorRecord by default, and how $PSNativeCommandUseErrorActionPreference behaves. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15
-
Microsoft Learn, about_Preference_Variables. On $ErrorActionPreference defaulting to Continue, the -ErrorAction parameter taking precedence for an individual command, the setting applying to its scope and child scopes, $PSNativeCommandUseErrorActionPreference defaulting to $false, and the example of temporarily disabling it inside a script block for a command like robocopy that uses a nonzero exit code as information. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, What’s New in PowerShell 7.4. On the experimental feature PSNativeCommandErrorActionPreference ($PSNativeCommandUseErrorActionPreference) becoming a mainstream feature in PowerShell 7.4. ↩ ↩2 ↩3
-
Microsoft Learn, Everything you wanted to know about exceptions. On accessing exception information from $_ inside a catch block, how a command with -ErrorAction Stop and a Write-Error error both become handleable in catch, and the try/finally resource-cleanup pattern. ↩ ↩2
-
Microsoft Learn, about_Language_Keywords. On the exit keyword setting the exit code and also reflecting it into $LASTEXITCODE, a script launched with pwsh -File returning exit’s numeric argument as its exit code, and completing normally with 0 or with 1 on an unhandled exception when there’s no exit statement. ↩ ↩2 ↩3
-
Microsoft Learn, about_Pwsh. On how the exit code is determined when launched with -File, and how launching with -Command converts any exit code other than 0 or 1 into 1, meaning you need exit $LASTEXITCODE to preserve it. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_Try_Catch_Finally. On try/catch/finally syntax, type-specific and multiple catch blocks, and how a finally block runs on success, on error, when stopped with Ctrl+C, and even when exit is called inside catch. ↩
-
Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. On how PowerShell 7 changed things so that $? no longer becomes $false just because a native command wrote to stderr, only doing so on a nonzero exit code. ↩
-
Microsoft Learn, about_Automatic_Variables. On $LASTEXITCODE holding the exit code of a native program or script, and how it’s set to 1 on an exception exit, to the exit keyword’s value, or to 0 on normal completion when called via pwsh -File. ↩
-
Microsoft Learn, Start-Transcript. On recording a session’s commands and console output to a text file, appending with -Append, the default save location and filename, and stopping with Stop-Transcript. ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
PowerShell Execution Policy and Script Signing — A Practical Guide to Graduating From "Papering Over It With Bypass"
PowerShell's execution policy is "a safety feature, not a security boundary." This article covers the differences between policies like R...
Applied PowerShell Scripting — Safely Automating Log Investigation, Archiving, and Reporting
Practical steps for safely automating log investigation, CSV reporting, archiving old logs, keeping audit trails, and Task Scheduler exec...
When Task Scheduler Tasks Don't Run or Exit with 0x1 — Isolating the Cause and Designing for Reliable Operation
A design guide to putting Windows Task Scheduler into reliable production use: execution accounts and logon types, what 'Run whether user...
How to Run PowerShell from C# (CSharp) and Receive the Results as Objects
How to launch PowerShell from C# and receive results as PSObject rather than strings — a practical walkthrough of the PowerShell SDK, Add...
Testing PowerShell with Pester — A Practical Approach to Making Operations Scripts Harder to Break
A practical walkthrough of testing PowerShell scripts with Pester v5 — safely covering date handling, file operations, deletion logic, mo...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- I wrote a try/catch in PowerShell, but why doesn't execution ever reach the catch block?
- Because most of the errors cmdlets raise are non-terminating errors. try/catch only catches terminating errors; a non-terminating error displays a message and lets the pipeline continue, so it never reaches catch. The standard fix is to add -ErrorAction Stop to the command you want to catch (or set $ErrorActionPreference = 'Stop' at the top of the script). This promotes the non-terminating error into a terminating one so try/catch can handle it.
- How should I decide between $? and $LASTEXITCODE?
- $? is a Boolean that shows whether the immediately preceding operation succeeded; it gets set for both cmdlets and native commands. $LASTEXITCODE is the exit code of the last native program that ran (or the last script that called exit), and it doesn't change on cmdlet errors. When judging whether an external command like robocopy or git succeeded, $LASTEXITCODE is the reliable choice, since you can also check what the specific exit code actually means. Keep in mind that a nonzero exit code from a native command does not, by default, land in a catch block.
- How do I let Task Scheduler judge whether a PowerShell script succeeded or failed?
- Set an explicit exit code with the exit keyword at the end of the script (and in each catch block), and have the task launch it with pwsh -File (or powershell.exe -File) so you can monitor the 'Last Run Result' value. When launched with -File, whatever value you pass to exit becomes the process's exit code as-is; without an exit statement, a normal completion returns 0 and an unhandled exception returns 1. Launching with -Command instead collapses any exit code other than 0 or 1 down to 1, so if you're building operations around exit codes, launching with -File is the standard approach.
- What kinds of errors should be retried?
- Limit retries to transient errors where trying again could actually change the outcome — a momentary network blip, a file that's temporarily locked, waiting for a dependent service to start, and so on. Business errors and permanent errors, like invalid input data, insufficient permissions, or misconfiguration, will fail no matter how many times you retry, so let them fail immediately instead and notify a human through logs and the exit code. Even when you do retry, you need an upper bound on the count and the interval, widening intervals with exponential backoff, and the underlying assumption that the processing is designed to be idempotent so re-running it doesn't cause double processing.
- What does PowerShell 7.4's $PSNativeCommandUseErrorActionPreference setting do?
- It's a setting that raises a PowerShell error (a non-terminating error) when a native command finishes with a nonzero exit code. It was added as an experimental feature in PowerShell 7.3 and became a mainstream feature in 7.4 (the default is $false). Setting it to $true makes native-command failures follow $ErrorActionPreference, so combined with Stop, you can catch external-command failures in try/catch. That said, some commands, like robocopy, use nonzero exit codes as normal, meaningful information, so you need to take care to temporarily set it back to $false around those calls.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links