PowerShell Error Handling and Retry Design — From the try/catch Trap to Exit Codes and Retry Best Practices

· · 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 Stop to 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
  • -ErrorAction overrides $ErrorActionPreference for that one command. The two aren’t perfectly symmetric, though — -ErrorAction only 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 $false and lands in $LASTEXITCODE, but no ErrorRecord gets created and it never reaches catch. Judge success or failure with $LASTEXITCODE.1
  • In PowerShell 7.4, $PSNativeCommandUseErrorActionPreference became a mainstream feature. Set it to $true and 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. $_.Exception gives you the exception itself, and for a promoted error, $_.Exception.ErrorRecord takes 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 exit keyword, and if the script is launched with pwsh -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.

  • $_.Exception is the exception itself. An error promoted by -ErrorAction Stop gets wrapped in an ActionPreferenceStopException, 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.PositionMessage tells 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. exit also 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 -Command instead, and an exit code like exit 10 inside the script doesn’t survive. It gets rounded down to 0 or 1 based on whether the last command succeeded (though if you write exit 10 directly in the command string itself, that value is returned as-is). If your operations depend on distinguishing between different script exit codes, launching with -File is 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 Stop explicitly to any command you want to catch.
  • $ErrorActionPreference defaults to Continue. Set an unattended script’s $ErrorActionPreference to 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 $PSNativeCommandUseErrorActionPreference to 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 to exit becomes 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.

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.

References

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

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

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

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

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

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

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

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

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

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

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

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.

Back to the Blog