Loosely Decoupled
TroubleshootingWindows ServerAutomation

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.

A tired man in headphones happily holds up a stamped approval slip while a small monkey with an ID badge keeps stamping. Behind them, parcels slide off the belt end and burst on the floor.
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:

PowerShell
aws --version
$Code = $LASTEXITCODE
if ($Code -ne 0) { throw "AWS CLI exited with code $Code" }

For a cmdlet, use PowerShell error handling:

PowerShell
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:

PowerShell
cmd.exe /d /c exit 0
Write-Error 'Demonstration: the upload failed' -ErrorAction Continue
"Native exit code: $LASTEXITCODE"

The error appears, followed by:

Text
Native exit code: 0

The 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:

PowerShell
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:

PowerShell
$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:

PowerShell
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:

PowerShell
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

New articles, by email.

A short note when something new is published.