Skip to content

PowerShell Return Codes

How PowerShell uses $? and $LASTEXITCODE to track command success/failure and native process exit codes for robust automation and CI/CD pipelines.

In automation and systems administration, evaluating whether an operation completed successfully is critical for workflow orchestration, conditional logic, and CI/CD pipelines. A return code (also called an exit code) is a numeric value returned by a program or script to indicate whether it completed successfully or encountered an error. PowerShell tracks this information through two distinct automatic variables: $? for the success or failure status of any command, and $LASTEXITCODE for the integer return code emitted by external native executables.

These mechanisms behave differently from the exception system used by .NET code, and understanding the distinction is essential for writing robust automation.

Command Status: The $? Automatic Variable

The $? automatic variable contains the execution status of the immediately preceding command, expression, or pipeline. It returns a boolean value:

  • $true: The last operation executed without error.
  • $false: The last operation failed, encountered an error (even non-terminating), or an external binary returned a non-zero exit code.
test-status.ps1powershell
Get-Item -Path "$env:WINDIR" | Out-Null
Write-Output "Get-Item Success Status: $?"

Get-Item -Path "C:\NonExistentPath\MissingFile.txt" -ErrorAction SilentlyContinue
Write-Output "Get-Item Failure Status: $?"

where:

  • Out-Null discards the pipeline object stream while preserving the execution status in $?.
  • -ErrorAction SilentlyContinue hides error messages on the screen while still recording failure ($false) in $?.
Consolepowershell
pwsh ./test-status.ps1
Outputplaintext
Get-Item Success Status: True
Get-Item Failure Status: False

Native Process Return Codes: $LASTEXITCODE

While $? returns a boolean for any PowerShell statement, $LASTEXITCODE specifically records the integer exit code returned by the last native executable (such as ping.exe, git.exe, robocopy.exe, or custom compiled binaries). It is an automatic variable that stores the exit code of the last external program or script that was run, not PowerShell cmdlets, but native executables or another .ps1 script invoked externally.

By standard operating system convention:

  • 0: Indicates normal, successful termination.
  • Non-zero integer (1, 2, 127, etc.): Indicates an error, invalid argument, or warning state.
eval-exit-code.ps1powershell
ping.exe -n 1 192.0.2.1 | Out-Null

[PSCustomObject]@{
    ExecutableName  = "ping.exe"
    NumericExitCode = $LASTEXITCODE
    BooleanSuccess  = $?
}

where:

  • ping.exe is an external native binary executed by PowerShell.
  • $LASTEXITCODE holds the integer exit code emitted by the process upon termination.
  • BooleanSuccess displays $?, which automatically evaluates to $false whenever $LASTEXITCODE is non-zero.
Consolepowershell
pwsh ./eval-exit-code.ps1
Outputplaintext
ExecutableName NumericExitCode BooleanSuccess
-------------- --------------- --------------
ping.exe                     1          False

Non-Zero Success Codes (Robocopy Example)

Some command-line tools use non-zero exit codes to communicate valid operational conditions rather than critical errors. For instance, Robocopy uses exit codes between 1 and 7 to indicate varying levels of successful file copies:

Robocopy Exit Code Meaning Severity
0 No files were copied; destination is synchronized. Success
1 One or more files copied successfully. Success
2 Extra files or directories detected in destination. Informational / Success
3 Files were copied and extra files were detected. Informational / Success
8 Some files or directories could not be copied. Error
16 Fatal error; no files copied. Error

When working with tools like Robocopy, do not rely on $? alone, as PowerShell flags any code $\ge 1$ as $false. Instead, validate $LASTEXITCODE using numeric relational operators:

robocopy.exe C:\Source C:\Backup /MIR | Out-Null
if ($LASTEXITCODE -ge 8) {
    Write-Error "Robocopy failed with exit code: $LASTEXITCODE"
} else {
    Write-Output "Robocopy completed successfully (Exit Code: $LASTEXITCODE)."
}

The exit Statement and Script Exit Codes

When running scripts inside scheduled tasks, CI/CD runners (such as GitHub Actions or Azure Pipelines), or external shell invocations, your script must emit a standard integer exit code so parent processes know whether the task succeeded.

The exit statement stops script execution and returns an explicit integer exit code:

exit <Int32>

exit vs. return

It is important to distinguish between return and exit:

Statement Scope Typical Use Case
return Function or script block Exits the local scope, returns data to caller, and continues script execution.
exit Entire script / process Terminates script execution immediately and emits an exit code to the operating system.
validate-prerequisite.ps1powershell
param (
    [string]$ConfigPath
)

if (-not (Test-Path -Path $ConfigPath)) {
    Write-Error "Required configuration file '$ConfigPath' is missing."
    exit 10
}

Write-Output "Configuration located. Initializing..."
exit 0

where:

  • exit 10 halts the script immediately and sets the return code to 10.
  • exit 0 explicitly notifies calling systems that execution succeeded.

Invoke the script from an external CLI session:

Consolepowershell
pwsh -NoProfile -File ./validate-prerequisite.ps1 -ConfigPath "C:\Missing\app.json"
Write-Output "Parent shell captured exit code: $LASTEXITCODE"
Outputplaintext
validate-prerequisite.ps1: Required configuration file 'C:\Missing\app.json' is missing.
Parent shell captured exit code: 10

Return Codes vs. .NET Exceptions

In traditional shell environments like Batch (ERRORLEVEL) or Bash ($?), integer return codes are the primary error-handling mechanism. PowerShell, by contrast, relies primarily on the .NET Exception model, grouping errors into structured ErrorRecord objects that carry stack traces, target objects, and category details.

Architectural Comparison

Dimension Process Return Codes ($LASTEXITCODE) PowerShell .NET Exceptions ($Error, try/catch)
Source External native executables (.exe, .bin, .sh) PowerShell cmdlets, functions, and .NET runtime APIs
Data Payload Single signed 32-bit integer Rich ErrorRecord object with metadata and stack trace
Control Flow Manual conditional checks (if ($LASTEXITCODE -ne 0)) Structured blocks (try { } catch { } finally { })
Default Handling Execution continues unless explicitly checked Can be configured via -ErrorAction (Stop, Continue, etc.)

For a quick reference, the three mechanisms differ as follows:

Mechanism Type Captures
$LASTEXITCODE Integer exit code Native/external program exit status
$? Boolean flag Success/failure of the last command (cmdlet or external)
.NET Exceptions (try/catch) Exception object Detailed error information: message, stack trace, exception type
exception-example.ps1powershell
try {
    Get-Item "C:\DoesNotExist" -ErrorAction Stop
} catch {
    Write-Output "Caught an exception: $($_.Exception.Message)"
}

where:

  • -ErrorAction Stop forces a normally non-terminating error to become a terminating exception, which try/catch can intercept.
  • $_.Exception.Message provides a detailed, human-readable error description, which is far richer than a simple 0/1 status code.
Consolepowershell
./exception-example.ps1
Outputplaintext
Caught an exception: Cannot find path 'C:\DoesNotExist' because it does not exist.

Converting Native Exit Codes to .NET Exceptions

Because external command failures do not throw .NET exceptions, native command failures will not trigger standard try/catch blocks on their own:

try {
    ping.exe -n 1 192.0.2.1
} catch {
    Write-Output "Caught failure!" # Never executed
}

To integrate external tools into structured exception handling, manually check $LASTEXITCODE and throw:

bridge-exit-code.ps1powershell
try {
    ipconfig.exe /invalidswitch | Out-Null

    if ($LASTEXITCODE -ne 0) {
        throw "Native utility failed with exit code $LASTEXITCODE."
    }
}
catch {
    Write-Warning "Caught Exception: $_"
}

where:

  • throw converts a non-zero exit code into a terminating exception.
  • catch intercepts the exception and reads the message using $_.
Consolepowershell
pwsh ./bridge-exit-code.ps1
Outputplaintext
WARNING: Caught Exception: Native utility failed with exit code 1.

Practical Example: CI/CD Build and Packaging Step

The following script automates a file packaging step using the native tar.exe utility, verifies return codes and $?, and terminates with an appropriate exit code for a CI/CD runner:

package-build-artifact.ps1powershell
param (
    [string]$SourceFolder = "$PSScriptRoot",
    [string]$ArtifactFile = "$env:TEMP\build-artifact.tar"
)

if (-not (Test-Path -Path $SourceFolder)) {
    Write-Error "Source build folder does not exist: $SourceFolder"
    exit 2 # Exit code 2: Input missing
}

Write-Output "Packaging build artifacts using native tar utility..."

tar.exe -cf "$ArtifactFile" -C "$SourceFolder" . 2>$null

if ($LASTEXITCODE -ne 0 -or -not $?) {
    Write-Error "Packaging failed! tar.exe exited with code $LASTEXITCODE."
    exit 1 # Exit code 1: Native binary failed
}

$Artifact = Get-Item -Path $ArtifactFile -ErrorAction SilentlyContinue

if ($Artifact -and $Artifact.Length -gt 0) {
    [PSCustomObject]@{
        ArtifactName = $Artifact.Name
        SizeBytes    = $Artifact.Length
        CreatedAt    = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
        Status       = "Artifact verified successfully"
    }
    exit 0 # Exit code 0: Clean success
} else {
    Write-Error "Artifact was generated but is empty or missing."
    exit 3 # Exit code 3: Integrity verification failed
}

where:

  • Test-Path checks prerequisites and calls exit 2 if validation fails.
  • tar.exe runs with standard error suppressed (2>$null).
  • if ($LASTEXITCODE -ne 0 -or -not $?) verifies that the native command finished cleanly.
  • Get-Item checks the final output artifact size, and exit 0 signals overall success.

Run the script and check the parent return code:

Consolepowershell
pwsh -NoProfile -File ./package-build-artifact.ps1
Write-Output "Script finished with exit code: $LASTEXITCODE"
Outputplaintext
Packaging build artifacts using native tar utility...

ArtifactName        SizeBytes CreatedAt           Status
------------        --------- ---------           ------
build-artifact.tar      10240 2026-10-24 17:35:10 Artifact verified successfully

Script finished with exit code: 0
Navigation