A PowerShell script runs an external program such as git inside a try block. The program fails and prints its error text, but the catch block never runs and the script carries on. Why do external programs behave differently from cmdlets, and how should the script detect the failure?
answer
- a separate process, not a cmdlet
- success is a number, not a record
- $LASTEXITCODE and $?
- check it yourself and throw
basics
~20 sAn external program is a separate process that reports failure with an exit code, not with a PowerShell error record, so try/catch and $ErrorActionPreference never see it. Check $LASTEXITCODE after the call and throw yourself.
solid answer
~50 sCmdlets report failures as `ErrorRecord` objects on PowerShell's error stream, which is what `$ErrorActionPreference`, `-ErrorAction` and `try/catch` all operate on. An external program is just another process: it writes text to stdout and stderr and reports success or failure through its **process exit code**. PowerShell surfaces that code in `$LASTEXITCODE` and reflects it in `$?`, but a non-zero exit is not an error record, so nothing in the exception machinery reacts to it. The portable pattern is to check explicitly — `if ($LASTEXITCODE -ne 0) { throw "git push failed ($LASTEXITCODE)" }` — which converts the failure into a real terminating error your `catch` can handle. PowerShell 7.4 adds `$PSNativeCommandUseErrorActionPreference`, which when enabled makes a non-zero exit raise an error subject to `$ErrorActionPreference`; I still write the explicit check for scripts that must run on Windows PowerShell 5.1 too.
code
powershell · 4 linesgit push origin main
if ($LASTEXITCODE -ne 0) {
throw "git push failed with exit code $LASTEXITCODE"
}go deeper
Know that an external program reports success or failure with a numeric exit code, and that PowerShell exposes that code in $LASTEXITCODE right after the call.
Explain why a non-zero exit is not an ErrorRecord, so $ErrorActionPreference and try/catch cannot see it, and show the explicit check-and-throw pattern that bridges the gap.
Diagnose the real script: separate a tool writing progress to stderr from genuine failure, check $LASTEXITCODE immediately before anything overwrites it, and know $PSNativeCommandUseErrorActionPreference exists in PowerShell 7.4.
Set the standard for wrapping external tools in shared automation — one helper that checks exit codes uniformly, per-tool success ranges such as robocopy's, and how the script's own exit code drives CI status.
## Two different failure protocols PowerShell's error handling is built around one object type: the `ErrorRecord` written to the error stream. `-ErrorAction`, `$ErrorActionPreference`, `-ErrorVariable`, `$Error` and `try/catch` are all machinery around that object. An external program — `git`, `robocopy`, `docker`, `kubectl`, `msbuild` — participates in none of it. It is a separate operating-system process, and it obeys the ancient process contract instead: - **stdout** — its normal output, which PowerShell captures as a stream of strings. - **stderr** — a second text stream, conventionally used for diagnostics. - **exit code** — an integer returned when the process ends; zero conventionally means success. PowerShell records that integer in the automatic variable `$LASTEXITCODE`, and sets `$?` to `$false` when the code is non-zero. That is the whole of the signal. Because a non-zero exit produces no error record, `try/catch` has nothing to catch, `$ErrorActionPreference = 'Stop'` has nothing to escalate, and `-ErrorAction Stop` cannot even be applied — an external program does not accept PowerShell common parameters, so `git push -ErrorAction Stop` simply hands `git` two more arguments it does not understand. ## The explicit check The pattern that works on every PowerShell version: ```powershell git push origin main if ($LASTEXITCODE -ne 0) { throw "git push failed with exit code $LASTEXITCODE" } ``` Two cautions. `$LASTEXITCODE` reflects the most recent *native* command only, and it is overwritten by the next one — so check it immediately, before any other external call, and capture it into a local variable if you need it later. And `$?` is even more fragile: it reflects only the immediately preceding command, cmdlet or native, so an intervening `Write-Host` destroys it. In a script that shells out repeatedly, wrap the check once: ```powershell function Invoke-Native { param([scriptblock]$Command) & $Command if ($LASTEXITCODE -ne 0) { throw "native command failed with exit code $LASTEXITCODE" } } ``` ## PowerShell 7.4 and $PSNativeCommandUseErrorActionPreference Modern PowerShell narrows the gap. The preference variable `$PSNativeCommandUseErrorActionPreference` — introduced as an experimental feature in PowerShell 7.3 and no longer experimental in 7.4 — makes a native command that exits non-zero produce an error that honours `$ErrorActionPreference`. With it enabled and the preference set to `Stop`, a failing `git` call becomes a catchable terminating error like any cmdlet failure. Check the value in your own environment rather than assuming it, and remember it does not exist at all in Windows PowerShell 5.1. Any script expected to run on both should keep the explicit `$LASTEXITCODE` check, which behaves identically everywhere. ## stderr is not failure The second half of this trap: text on stderr does **not** mean the program failed. Plenty of tools use stderr for progress and informational output precisely because it keeps stdout clean for pipeable data — `git` writes clone and push progress there, `ffmpeg` writes essentially everything there. Judging success by "did it print to stderr" produces false alarms constantly. The inverse also bites. In Windows PowerShell, redirecting a native command's stderr into the error stream with `2>&1` turns those lines into error records — and under `$ErrorActionPreference = 'Stop'` an ordinary progress message can then abort the script. Scripts that were fine interactively fail as soon as someone captures their output. If you need the combined text, capture it as strings rather than routing it through the error stream. ## Not every non-zero code means failure Exit-code conventions are per-tool, not universal. `robocopy` is the standard example: it uses a bit-mapped exit code where values below 8 report what it did — 1 means files were copied, 3 means copied plus extras — and only 8 and above indicate real failures. A blanket `-ne 0` check treats a perfectly successful robocopy run as a catastrophe. When you wrap a tool, encode *that tool's* notion of success: ```powershell robocopy $src $dst /E if ($LASTEXITCODE -ge 8) { throw "robocopy failed with exit code $LASTEXITCODE" } ``` ## Why this matters in CI The consequence of getting this wrong is not a noisy log — it is a green build that shipped nothing. A deployment script that runs six external tools and checks none of their exit codes will report success while every step failed, because the only thing PowerShell itself decided was that it reached the end of the file. Explicitly propagating native failure — and ending the script with a non-zero exit code of its own — is what makes the pipeline honest.
- Can you not just add -ErrorAction Stop to the git call?No. Common parameters are a PowerShell concept implemented by cmdlets and advanced functions. An external program is a separate process, so `-ErrorAction Stop` is passed straight through as two more command-line arguments for git to reject or misinterpret.
- A script checks whether the external tool wrote anything to stderr and treats that as failure. What is wrong with that?stderr is a diagnostics channel, not a failure signal. git writes push and clone progress there, ffmpeg writes nearly all its output there. The result is constant false alarms. The exit code is the tool's actual verdict; stderr is just text it chose not to put on stdout.
- Is checking for a non-zero exit code always the right failure test?No — conventions are per tool. robocopy uses a bit-mapped code where values under 8 describe what it copied and only 8 and above are genuine errors, so `-ne 0` would fail a successful run. Encode the specific tool's success range in the wrapper you write around it.
saying these in an interview costs you the question
- Expects try/catch to catch a non-zero exit code
- Adds -ErrorAction Stop to an external program
- Treats any stderr output as a failure
- Checks $LASTEXITCODE after several intervening commands
- Assumes every non-zero exit code means failure