Skip to content

/codes/powershell/lastexitcode-exit-codes

PowerShell exit codes: $LASTEXITCODE, $? and powershell.exe -File results

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
tool
PowerShell
versions
Windows PowerShell 5.1, PowerShell 7.x
os
windows, linux, macos

PowerShell has two status variables. $LASTEXITCODE is the exit code of the last native program (or script that called exit N); cmdlet errors do not change it. $? is $true or $false for the last command of any kind. When a CI job or scheduler runs powershell -File script.ps1, the process exit code is the number passed to exit, 1 if the script ended with an uncaught terminating error, and 0 otherwise, even if cmdlets wrote non-terminating errors.

The variables

A script that ends with exit N sets $LASTEXITCODE the same way a native program does:

powershell
'exit 3' | Set-Content exit3.ps1
'exit 0' | Set-Content exit0.ps1
& .\exit3.ps1
"script exit 3:  LASTEXITCODE=$LASTEXITCODE"
& .\exit0.ps1
"script exit 0:  LASTEXITCODE=$LASTEXITCODE"
& .\exit3.ps1
Get-Item missing.txt -ErrorAction SilentlyContinue
"failed cmdlet:  LASTEXITCODE=$LASTEXITCODE  ?=$?"

Output

script exit 3:  LASTEXITCODE=3
script exit 0:  LASTEXITCODE=0
failed cmdlet:  LASTEXITCODE=3  ?=False

The failed cmdlet sets $? to $false but leaves $LASTEXITCODE at the previous value. For native programs it works the same way: git status; $LASTEXITCODE.

Exit code of powershell.exe

Start-Process -Wait -PassThru returns the process, whose ExitCode is what a scheduler or CI runner sees:

powershell
'exit 7' | Set-Content exit7.ps1
'throw "boom"' | Set-Content throw.ps1
'Write-Error "soft error"' | Set-Content writeerror.ps1
'"all good"' | Set-Content ok.ps1
function Get-ExitCode([string[]] $ArgumentList) {
  (Start-Process powershell.exe -ArgumentList $ArgumentList -Wait -PassThru -NoNewWindow -RedirectStandardError err.txt -RedirectStandardOutput out.txt).ExitCode
}
foreach ($f in 'exit7', 'throw', 'writeerror', 'ok') {
  '-File {0,-15} -> {1}' -f "$f.ps1", (Get-ExitCode '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', ".\$f.ps1")
}
'-Command ending with a failed cmdlet -> ' + (Get-ExitCode '-NoProfile', '-NonInteractive', '-Command', 'Get-Item missing.txt')
'-Command ending with a good command  -> ' + (Get-ExitCode '-NoProfile', '-NonInteractive', '-Command', 'Get-Item missing.txt; Get-Date | Out-Null')

Output

-File exit7.ps1       -> 7
-File throw.ps1       -> 1
-File writeerror.ps1  -> 0
-File ok.ps1          -> 0
-Command ending with a failed cmdlet -> 1
-Command ending with a good command  -> 0
How the script endspowershell -File exit codepowershell -Command exit code
exit NNN
Uncaught terminating error (throw, -ErrorAction Stop)11
Non-terminating error only (Write-Error, failed cmdlet)01 if the last command failed ($? is false), else 0
Normal end00

Make CI fail on errors

build.ps1
$ErrorActionPreference = 'Stop'          # cmdlet errors become terminating -> exit code 1
dotnet build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }   # native tools need an explicit check

$ErrorActionPreference does not affect native programs. Check $LASTEXITCODE after each one, or in PowerShell 7.3+ set $PSNativeCommandUseErrorActionPreference = $true so a non-zero exit code becomes an error.

Sources