Search code examples
powershellerror-handlingexit-code

PowerShell and process exit codes


This self-answered question tries to address two distinct aspects of dealing with process exit codes in PowerShell:

  • In PowerShell code, how can you query the exit code set by an external process (a call to an external program), and how do such exit codes integrate with PowerShell's error handling?

  • When someone else calls PowerShell via its CLI, pwsh (PowerShell Core) / powershell.exe (Windows PowerShell), what determines the PowerShell process' exit code that communicates success vs. failure to the calling process (which could be a build / CI / automation-server task, a scheduled task, or a different shell, for instance).


Solution

  • Current as of PowerShell (Core) 7.4

    Note:

    • With respect to PowerShell-internal use of exit codes, this answer is focused on directly calling external (console, i.e. terminal-based) programs (which situationally requires the & operator for syntactic reasons), which is the typical use case.

    • External programs can also be called via the Start-Process cmdlet, which, however, is rarely the right tool (see GitHub docs issue #19379 for guidance on when it is required / appropriate).

      • If you do need to use Start-Process in a given scenario, see kiloalphaindia's answer for how to obtain the exit code ($LASTEXITCODE is not populated in that case).

    PowerShell-internal use of exit codes:

    PowerShell-internally, where native PowerShell commands generally run in-process, exit codes from child processes that run external programs play a very limited role:

    • Native PowerShell commands generally don't set exit codes and don't act on them.

    • PowerShell has an abstract counterpart to exit codes: $?, the automatic, Boolean success-status variable:

    • It reflects whether the most recently executed command had any errors, but in practice it is rarely used, not least because - up to version 6.x - something as seemingly inconsequential as enclosing a command in (...) reset $? to $true - see GitHub issue #3359 - and because using Write-Error in user functions doesn't set $? to $false - see GitHub issue #3629; however, eventually providing the ability for user code to set $? explicitly has been green-lit for a future version.

    • While $? also reflects (immediately afterwards) whether an external program reported an exit code of 0 (signaling success, making $? report $true) or a nonzero exit code (typically signaling failure, making $? $false), it is the automatic $LASTEXICODE variable that contains the specific exit code as an integer, and that value is retained until another external program, if any, is called in the same session.

      • Caveat: Due to cmd.exe's quirks, a batch file's exit code isn't reliably reported, but you can work around that with cmd /c <batch-file> ... `& exit - see this answer; GitHub issue #15143 additionally suggests building this workaround into PowerShell itself.

      • Also, up to v7.1, $? could report false negatives, namely if the external program reports exit code 0 while also producing stderr output and there is also a PowerShell redirection involving 2> or *> - see this answer and GitHub issue #3996; this has been corrected in v7.2+.

      • Finally, it's best to treat $LASTEXITCODE as read-only and let only PowerShell itself set it. (Technically, the variable is writeable and lives in the global scope, so if you do want to modify manually, after all, be sure to assign to $global:LASTEXITCODE, so as not accidentally create a transient local copy that has no effect.)

    • Unlike terminating errors or non-terminating errors reported by PowerShell-native commands, nonzero exit codes from external programs can not be automatically acted upon by the $ErrorActionPreference preference variable, invariably so in Windows PowerShell and PowerShell (Core) up to v7.3.x, and by default in v7.4+; that is, you cannot use that variable to silence stderr output from external programs nor can you, more importantly, choose to abort a script via value 'Stop' when an external program reports a nonzero exit code.

      • v7.4 introduced the $PSNativeCommandUseErrorActionPreference preference variable for opt-in integration of external-program calls with PowerShell's error handling: If you set this variable to $true, it will trigger a PowerShell error[1] whenever an external program reports a nonzero exit code, and that error is subject to the $ErrorActionPreference preference variable. That is, with $ErrorActionPreference = 'Stop' in effect, any external program reporting a nonzero exit code then causes the script to abort by default. (Stderr output prints either way, and separately requires 2>$null for silencing.)

    How to control what PowerShell reports as its exit code when it is called from the outside:

    Setting an exit code that at least communicates success (0) vs. failure (nonzero, typically) is an important mechanism for letting outside callers know whether your PowerShell code succeeded overall or not, such as when being called from a scheduled task or from an automation server such as Jenkins via the PowerShell CLI (command-line interface) - pwsh for PowerShell [Core] vs. powershell.exe for Windows PowerShell.

    The CLI offers two ways to execute PowerShell code, and you can use exit <n> to set an exit code, where <n> is the desired exit code:

    • -File <script> [args...] expects the path of a script file (*.ps1) to execute, optionally followed by arguments.

      • Executing exit <n> directly inside such a script file (not inside another script that you call from that script) makes the PowerShell process report its exit code as <n>.

      • If a given script file exits implicitly or with just exit (without an exit-code argument), exit code 0 is reported.

    • -Command <powershell-code> expects a string containing one or more PowerShell commands.

      • To be safe, use exit <n> as a direct part of that command string - typically, as the last statement; without that, the exit code is either 0 (success) or 1 (failure), as described below.

    If your code is called from tools that check success by exit code, make sure that all code paths explicitly use exit <n> to terminate.

    Caveat: If the PowerShell process terminates due to an unhandled script-terminating error - irrespective of whether the CLI was invoked with -File or -Command - the exit code is always 1.

    • A script-terminating (fatal) error is either generated from PowerShell code with the throw statement or by escalating a less a severe native PowerShell error with
      -ErrorAction Stop or $ErrorActionPreference = 'Stop', or by pressing Ctrl-C to forcefully terminate a script.

    • If exit code 1 isn't specific enough (it usually is, because typically only success vs. failure needs to be communicated), you can wrap your code in a try / catch statement and use exit <n> from the catch block.

    The exact rules for how PowerShell sets its process exit code are complex; find a summary below.


    How PowerShell sets its process exit code:

    • If an unhandled script-terminating error occurs, the exit code is always 1.

    • With -File, executing a script file (*.ps1):

      • If the script directly executes exit <n>, <n> becomes the exit code (such statements in nested calls are not effective).

      • Otherwise, it is 0, even if non-terminating or statement-terminating errors occurred during script execution.

    • With -Command, executing a command string containing one or more statements:

      • If an exit <n> statement is executed directly as one of the statements passed in the command string (typically, the last statement), <n> becomes the exit code.

      • Otherwise, it is the success status of the last statement executed, as implied by $?, that determines the exit code:

        • If $? is:

          • $true -> exit code 0
          • $false -> exit code 1 - even in the case where the last executed statement was an external program that reported a different nonzero exit code.
        • Given that the last statement in your command string may not be the one whose success vs. failure you want to signal, use exit <n> explicitly to reliably control the exit code, which also allows you to report specific nonzero exit codes.

          • For instance, to faithfully relay the exit code reported by an external program, append ; exit $LASTEXITCODE to the string you pass to -Command.

    Inconsistencies and pitfalls as of PowerShell 7.0:

    • Arguably, -Command (-c) should report the specific exit code of the last statement - provided it has one - instead of the abstract 0 vs. 1. For instance, pwsh -c 'findstr'; $LASTEXITCODE should report 2, findstr.exe's specific exit code, instead of the abstract 1 - see GitHub issue #13501.

    • Exit-code reporting with *.ps1 files / the -File CLI parameter:

      • It is only an explicit exit <n> statement that meaningfully sets an exit code; instead, it should again be the last statement executed in the script that determines the exit code (which, of course, could be an exit statement), as is the case in POSIX-compatible shells and with -Command, albeit in the suboptimal manner discussed.

      • When you call a *.ps1 script via -File or as the last statement via -Command, PowerShell's exit code in the absence of the script exiting via an exit statement is always 0 (except in the exceptional Ctrl-C / throw cases, where it becomes 1).

      • By contrast, when called in-session, again in the absence of exit, $LASTEXICODE reflects the exit code of whatever external program (or other *.ps1 if it set an exit code) was executed last - whether executed inside the script or even before.

      • In other words:

        • With -File, unlike with -Command, the exit code is categorically set to 0 in the absence of an exit statement (barring abnormal termination).
        • In-session, the exit code (as reflected in $LASTEXITCODE) is not set at all for the script as a whole in the absence of an exit statement.
      • See GitHub issue #11712.


    [1] Unfortunately, up at least PowerShell 7.4.0-preview.6, the automatically triggered PowerShell error is a non-terminating error rather than a statement-terminating one; the latter would make more sense, as it would allow it to be selectively caught with a try statement - see GitHub issue #18368.