/codes/powershell/lastexitcode-exit-codes
PowerShell exit codes: $LASTEXITCODE, $? and powershell.exe -File results
- 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:
'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:
'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 ends | powershell -File exit code | powershell -Command exit code |
|---|---|---|
| exit N | N | N |
| Uncaught terminating error (throw, -ErrorAction Stop) | 1 | 1 |
| Non-terminating error only (Write-Error, failed cmdlet) | 0 | 1 if the last command failed ($? is false), else 0 |
| Normal end | 0 | 0 |
Make CI fail on errors
$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
- learn.microsoft.comlearn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables
- learn.microsoft.comlearn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe
- learn.microsoft.comlearn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pwsh