Why $LASTEXITCODE Doesn't Work the Way You Expect in PowerShell
Cmdlets never set LASTEXITCODE, so after a failed cmdlet it still holds whatever the last native program returned, often zero.

On this page
TL;DR
$LASTEXITCODE is not the success status of the last PowerShell cmdlet. Use it
for a native executable such as aws.exe, and capture it immediately:
aws --version
$Code = $LASTEXITCODE
if ($Code -ne 0) { throw "AWS CLI exited with code $Code" }For a cmdlet, use PowerShell error handling:
Get-Item -LiteralPath 'C:\Data\settings.json' -ErrorAction Stop-ErrorAction Stop turns a cmdlet’s non-terminating error into a terminating
one, which stops the script or reaches an enclosing catch.
While working on a backup script for my 7 Days to Die game server, I thought it would be good to share this.
You replace an AWS CLI command with a PowerShell cmdlet and keep the success check. The cmdlet prints an error. The script announces success anyway.
The check still runs. It’s reading the result of a different operation.
Reproduce a failed command with exit code zero
Run this in PowerShell on Windows:
cmd.exe /d /c exit 0
Write-Error 'Demonstration: the upload failed' -ErrorAction Continue
"Native exit code: $LASTEXITCODE"The error appears, followed by:
Native exit code: 0The zero came from cmd.exe. Write-Error did not update it. The explicit
Continue makes this demonstration non-terminating even if your session normally
uses a stricter error preference.
The reverse is just as misleading:
cmd.exe /d /c exit 7
Get-Date | Out-Null
"Native exit code: $LASTEXITCODE"This prints 7 even though Get-Date succeeded. These examples illustrate why
a value left in the variable is not evidence about a later cmdlet.
Microsoft defines the variable for native programs and PowerShell scripts;
script behavior also depends on invocation and the exit keyword. Ordinary
cmdlet success or failure does not make it a universal status register. See
about_Automatic_Variables (opens in a new tab).
For native commands, check each result at the boundary
This example assumes AWS CLI is installed, the source exists, and the active AWS identity can access your chosen bucket:
$Source = 'C:\Data\exports'
$Destination = 's3://YOUR-BUCKET/exports/'
$null = Get-Command aws -CommandType Application -ErrorAction Stop
aws s3 sync $Source $Destination
$Code = $LASTEXITCODE
if ($Code -ne 0) {
throw "S3 sync did not complete cleanly; AWS CLI exit code $Code"
}
'Export transfer completed'Replace YOUR-BUCKET before running it. AWS documents nonzero outcomes including
failed S3 transfers and skipped files. A skipped file is enough to reject a
backup run as incomplete; inspect the command output for the cause. See the
AWS CLI return-code reference (opens in a new tab).
Warning
Do not run three transfers and check only after the third. A later successful native command replaces an earlier failure code with zero, so the script reports success for a backup that partly failed. That is worse than no success message at all: you now have positive evidence for something untrue, and you will not look again until you need the data.
Capture and handle each result before moving on.
Also distinguish a process failure from a launch failure. If aws is not
found, no AWS CLI process returned an exit code, and the check reads whatever
an earlier native command left. The Get-Command line stops the script before
that can happen.
Why try/catch sometimes misses a cmdlet error
PowerShell can report an error while continuing execution. For example:
try {
Write-Error 'Demonstration failure' -ErrorAction Continue
'Still inside try'
}
catch {
'Caught the error'
}The error is displayed and Still inside try prints. The catch block isn’t
entered. Change that one invocation to -ErrorAction Stop and the catch block
runs instead.
Some errors already terminate execution; they don’t need this promotion.
-ErrorAction Stop makes the intended behavior explicit for cmdlet errors that
would otherwise continue. Microsoft’s try/catch documentation (opens in a new tab)
explains which errors those blocks handle.
Catch a Write-S3Object failure with -ErrorAction Stop
With AWS.Tools.S3 installed and AWS access configured, use this for an
intentional upload of one existing file:
Import-Module AWS.Tools.S3 -ErrorAction Stop
try {
Write-S3Object -BucketName 'YOUR-BUCKET' `
-File 'C:\Data\exports\report.json' `
-Key 'exports/report.json' `
-ErrorAction Stop
'Upload completed'
}
catch {
Write-Warning ("Upload failed: {0}" -f $_.Exception.Message)
throw
}The success message is reachable only after the upload returns normally. The
bare throw propagates the caught error to the caller. Logging and then
continuing would hide the failure from whatever invoked the script.
If you have no context to add and no recovery to attempt, remove the try/catch
and keep -ErrorAction Stop. A catch block that only rethrows isn’t required.
$PSNativeCommandUseErrorActionPreference in PowerShell 7
PowerShell 7.4 added $PSNativeCommandUseErrorActionPreference. When it’s
$true, nonzero native exit codes produce PowerShell errors governed by
$ErrorActionPreference; with Stop, those can reach catch. The default is
$false. Microsoft documents it in
about_Preference_Variables (opens in a new tab).
That doesn’t make -ErrorAction Stop a parameter for aws.exe. Passed to a
native program, it’s an argument for that program to interpret. Explicit
exit-code checks remain useful for scripts spanning Windows PowerShell 5.1 and
PowerShell 7, and for tools whose nonzero codes have special meanings.
If native error promotion is enabled with Stop, execution can leave the native
invocation before the following explicit check. Either path must preserve the
failure; don’t catch it and then print success.
Common failures
LASTEXITCODE is empty or stale. There may be no relevant native result in the session. Choose the error mechanism for the operation you actually ran.
Can I replace it with $? everywhere? $? describes the last command’s
execution status and is overwritten by the next command. It isn’t a durable
record of a multi-step job. Structured error handling is clearer for cmdlets.
The task still reports success. Check whether a wrapper catches and suppresses
the error, then exits normally. At the outer script boundary, preserve a failure
through an unhandled terminating error or an intentional nonzero exit code.
A script needs to remember whether the work succeeded. That starts by listening to the failure signal the command actually provides.
Tested with
- OS
- Windows Server 2019 (10.0.17763)
- PowerShell
- Windows PowerShell 5.1
- PowerShell 7 section
- 7.6.6 on macOS 27.0
- Date
- October 2026