435

I want my PowerShell script to stop when any of the commands I run fail (like set -e in bash). I'm using both Powershell commands (New-Object System.Net.WebClient) and programs (.\setup.exe).

2
  • 1
    See also Equivalent of bash set -e · Issue #3415 · PowerShell/PowerShell. Commented Oct 1, 2021 at 15:26
  • 12
    In PowerShell 7.3.0-preview.1, set $PSNativeCommandUseErrorActionPreference to $true and $ErrorActionPreference to 'Stop' can stop the script execution when native command error occurs. Test code: & { $PSNativeCommandUseErrorActionPreference = $true; $ErrorActionPreference = 'Stop'; ping.exe bbb; ping.exe aaa } . Commented Jan 21, 2022 at 16:44

13 Answers 13

501

$ErrorActionPreference = "Stop" will get you part of the way there (i.e. this works great for cmdlets).

However for EXEs you're going to need to check $LastExitCode yourself after every exe invocation and determine whether that failed or not. Unfortunately I don't think PowerShell can help here because on Windows, EXEs aren't terribly consistent on what constitutes a "success" or "failure" exit code. Most follow the UNIX standard of 0 indicating success but not all do. Check out the CheckLastExitCode function in this blog post. You might find it useful.

Sign up to request clarification or add additional context in comments.

15 Comments

Does $ErrorActionPreference = "Stop" work for well-behaved programs (that return 0 on success)?
No, it doesn't work at all for EXEs. It only works for PowerShell cmdlets which run in-process. It is kind of pain but you have to check $LastExitCode after every EXE invocation, check that against the expected exit code and if that test indicates failure, you have to throw to terminate execution of the script e.g. throw "$exe failed with exit code $LastExitCode" where $exe is just the path to the EXE.
note that psake has a commandlet called "exec" which can you can use to wrap calls to external programs with a check for LastExitCode and display an error (and stop, if desired)
@Keith Hill: but not if you source it (what I do a lot and maybe others) via $ErrorActionPreference = "Continue" ; . ./subscript.ps1 ; echo "$ErrorActionPreference" (and subscript.ps1 will set it to $ErrorActionPreference = "Stop" it will output stop! So it still seems a good practice to me.
Many years later, there's now an RFC for introducing abort-on-failure support for external programs too.
|
125

You should be able to accomplish this by using the statement $ErrorActionPreference = "Stop" at the beginning of your scripts.

The default setting of $ErrorActionPreference is Continue, which is why you are seeing your scripts keep going after errors occur.


Before powershell 7.4.0, this only affected cmdlets and not native programs that set a non-zero exit code.

But now in 7.4, the PSNativeCommandErrorActionPreference experimental feature is enabled by default, so when $PSNativeCommandUseErrorActionPreference = $true then $ErrorActionPreference = "Stop" will stop execution even for native programs.

Now the script:

$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$PSNativeCommandUseErrorActionPreference = $true # might be true by default

cmd /c "exit 1"
"something else"

Will fail with

NativeCommandExitException: C:\test.ps1:4:1
Line |
   4 |  cmd /c "exit 1"
     |  ~~~~~~~~~~~~~~~
     | Program "cmd.exe" ended with non-zero exit code: 1.

and won't execute "something else".

2 Comments

This is an incomplete answer, however it was posted prior to the currently selected answer which builds on it. So it deserve its upvotes too, imho.
"at the beginning of your scripts" <- after the param block
60

Sadly, due to buggy cmdlets like New-RegKey and Clear-Disk, none of these answers are enough. I've currently settled on the following code in a file called ps_support.ps1:

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$PSDefaultParameterValues['*:ErrorAction']='Stop'
function ThrowOnNativeFailure {
    if (-not $?)
    {
        throw 'Native Failure'
    }
}

Then in any powershell file, after the CmdletBinding and Param for the file (if present), I have the following:

$ErrorActionPreference = "Stop"
. "$PSScriptRoot\ps_support.ps1"

The duplicated ErrorActionPreference = "Stop" line is intentional. If I've goofed and somehow gotten the path to ps_support.ps1 wrong, that needs to not silently fail!

I keep ps_support.ps1 in a common location for my repo/workspace, so the path to it for the dot-sourcing may change depending on where the current .ps1 file is.

Any native call gets this treatment:

native_call.exe
ThrowOnNativeFailure

Having that file to dot-source has helped me maintain my sanity while writing powershell scripts. :-)

Comments

23

A slight modification to the answer from @alastairtree:

function Invoke-Call {
    param (
        [scriptblock]$ScriptBlock,
        [string]$ErrorAction = $ErrorActionPreference
    )
    & @ScriptBlock
    if (($lastexitcode -ne 0) -and $ErrorAction -eq "Stop") {
        exit $lastexitcode
    }
}

Invoke-Call -ScriptBlock { dotnet build . } -ErrorAction Stop

The key differences here are:

  1. it uses the Verb-Noun (mimicing Invoke-Command)
  2. implies that it uses the call operator under the covers
  3. mimics -ErrorAction behavior from built in cmdlets
  4. exits with same exit code rather than throwing exception with new message

3 Comments

How do you pass parameters / variables? e.g. Invoke-Call { dotnet build $something }
@MichaelBlake the inquiry of yours is so right, allowing a params passthrough would make this approach gold. I am inspecting adamtheautomator.com/pass-hashtables-invoke-command-argument to adjust the Invoke-Call to support params pass-through. If I succeed, I will post it as another answer here.
Why do you use the splatting operator during the invoke? What does that get you? & @ScriptBlock and & $ScriptBlock appear to do the same thing. Haven't been able to google what the difference is in this case
17

You need slightly different error handling for powershell functions and for calling exe's, and you need to be sure to tell the caller of your script that it has failed. Building on top of Exec from the library Psake, a script that has the structure below will stop on all errors, and is usable as a base template for most scripts.

Set-StrictMode -Version latest
$ErrorActionPreference = "Stop"


# Taken from psake https://github.com/psake/psake
<#
.SYNOPSIS
  This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode
  to see if an error occcured. If an error is detected then an exception is thrown.
  This function allows you to run command-line programs without having to
  explicitly check the $lastexitcode variable.
.EXAMPLE
  exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed"
#>
function Exec
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd,
        [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ("Error executing command {0}" -f $cmd)
    )
    & $cmd
    if ($lastexitcode -ne 0) {
        throw ("Exec: " + $errorMessage)
    }
}

Try {

    # Put all your stuff inside here!

    # powershell functions called as normal and try..catch reports errors 
    New-Object System.Net.WebClient

    # call exe's and check their exit code using Exec
    Exec { setup.exe }

} Catch {
    # tell the caller it has all gone wrong
    $host.SetShouldExit(-1)
    throw
}

3 Comments

Invoking eg: Exec { sqlite3.exe -bail some.db "$SQL" }, the -bail causes an error since it's trying to interpret it as a Cmdlet parameter? Wrapping things in quotes doesn't seem to work. Any ideas?
Is there a way to put this code somewhere central so you can do some kind of #include when you want to use the Exec method?
Yeah, you can. You can put it a file named powershell-error-handling-sanity.ps1 and then dot-source the ps1 file at t he top of any other ps1 file with . <relative_or_absolute_path_to_powershell-error-handling-sanity.ps1
17

I'm new to powershell but this seems to be most effective:

doSomething -arg myArg
if (-not $?) {throw "Failed to doSomething"}

Comments

7

for people coming here on 2021 this is my solution that covers both cmdlets and programs

function CheckLastExitCode {
    param ([int[]]$SuccessCodes = @(0))

    if (!$?) {
        Write-Host "Last CMD failed $LastExitCode" -ForegroundColor Red
        #GoToWrapperDirectory in my code I go back to the original directory that launched the script
        exit
    }

    if ($SuccessCodes -notcontains $LastExitCode) {
        Write-Host "EXE RETURNED EXIT CODE $LastExitCode" -ForegroundColor Red
        #GoToWrapperDirectory in my code I go back to the original directory that launched the script
        exit
    } 
    
}

you can use it like this

cd NonExistingpath
CheckLastExitCode

Comments

6

As far as I know, Powershell does not have any automatic handling of non-zero exit codes returned by sub-programs it invokes.

The only solution I know about so far to mimick the behavior of bash -e is to add this check after every call to an external command:

if(!$?) { Exit $LASTEXITCODE }

5 Comments

I like this solution, it's just one line to add after each external program call.
@bounav: Well, this is relative. Let's just say it's the "less worse" solution I've found. On the other hand in a bash script you just add #!/bin/bash -e at the beginning of each script and it applies to the whole script all the time. I really really hate Powershell just for that stupid error handling. (But unfortunately I don't have much choice using it sometimes...)
I can't believe it's almost 2024 and I need to add this ugly little block of code after every single *.exe call in my deployment script just to get it to ACTUALLY fail in Azure DevOps when sc.exe gives a non-zero return code installing a Windows service! There are more elegant ways for me to solve this problem, but they seem to have taken 4+ years of community debate and have not yet left the experimental phase, let alone become available in a default Windows Server 2022 image running PowerShell 5.1.
Well, in the specific context of CI systems, there are some kind of solutions. First of in both Azure Devops and Github Actions you can actually use bash on Windows if you want (example doc for Azure: learn.microsoft.com/en-us/azure/devops/pipelines/tasks/… ). I think they both use git bash, which is kind of nice. Of course it's equivalent to just throwing away Powershell (a quite sane approach in my opinion).
Also on Gitlab CI, which I use professionally, while it doesn't have bash support on Windows (I tried) it does use some kind of magic on top of powershell to add automatic script stopping on errors, thus making powershell equivalent to a bash -e. (Don't ask me how they do that.) Not a bad solution either in the end (although I would prefer to be able to use bash on windows).
2

I came here looking for the same thing. $ErrorActionPreference="Stop" kills my shell immediately when I'd rather see the error message (pause) before it terminates. Falling back on my batch sensibilities:

IF %ERRORLEVEL% NEQ 0 pause & GOTO EOF

I found that this works pretty much the same for my particular ps1 script:

Import-PSSession $Session
If ($? -ne "True") {Pause; Exit}

Comments

2

Seems like simple rethrow does the trick.

param ([string] $Path, [string] $Find, [string] $Replace)
try {
  ((Get-Content -path $Path -Raw) -replace $Find, $Replace) | Set-Content -Path $Path
  Write-Output Completed.
} catch {
  # Without try/catch block errors don't interrupt program flow.
  throw
}

Now output Completed appears only after successful execution.

Comments

2

I always use a helper function like this to invoke any "classic" binaries (.exe) that don't throw errors/exceptions that powershell understands:

function _(
    [Parameter(Mandatory=$True)][string]$exe, 
    [Parameter(Mandatory=$True, ValueFromRemainingArguments=$True)][string[]]$arguments
) {
    &$exe @arguments
    if ($LASTEXITCODE -ne 0) { throw "ERROR running '$exe $(($arguments) -join ' ')' failed with LASTEXITCODE=$LASTEXITCODE" }
}

# usage:
_ python --version

Comments

1

Redirecting stderr to stdout seems to also do the trick without any other commands/scriptblock wrappers although I can't find an explanation why it works that way..

# test.ps1

$ErrorActionPreference = "Stop"

aws s3 ls s3://xxx
echo "==> pass"

aws s3 ls s3://xxx 2>&1
echo "shouldn't be here"

This will output the following as expected (the command aws s3 ... returns $LASTEXITCODE = 255)

PS> .\test.ps1

An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
==> pass

Comments

-1

I recommend to apply simple exit code validation. Depends on how sophisticated solution you need.

Best for single validation - stops only when docker-compose fails:

docker-compose up --detach || exit 1

Good if you want to stop the script and propagate error message

docker-compose up --detach || throw "Script has failed, check logs above"

or more complex and reusable function:

function terminate() {
    Write-Error "Found exit code: $LASTEXITCODE"
    throw "Last executed operation has failed, check logs above!"
}

docker-compose up --detach || terminate

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.