MicrosoftDocs / PowerShell-Docs

The official PowerShell documentation sources
https://learn.microsoft.com/powershell
Other
2k stars 1.58k forks source link

Our Error Handling, Ourselves - time to fully understand and properly document PowerShell's error handling #1583

Open mklement0 opened 7 years ago

mklement0 commented 7 years ago

The existing help topics that touch on error handling (about_Throw, about_CommonParameters, about_Preference_Variables, about_Trap, about_Try_Catch_Finally ):

It's time to:

Below is my understanding of how PowerShell's error handling actually works as of Windows PowerShell v5.1 / PowerShell Core v7.3.4, which can serve as a starting point for about_Error_Handling, along with links to issues to related problems.

Do tell me if and where I got things wrong. The sheer complexity of PowerShell's current error handling is problematic, though I do realize that making changes in this area is a serious backward-compatibility concern.



Example use of $PSCmdlet.WriteError() in an advanced function so as to create a non-terminating error (to work around the issue that Write-Error doesn't set$? to $False in the caller's context):

# PSv5+, using the static ::new() method to call the constructor.
# The specific error message and error category are sample values.
& { [CmdletBinding()] param()  # quick mock-up of an advanced function

  $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
      # The underlying .NET exception: if you pass a string, as here,
      #  PS creates a [System.Exception] instance.
      "Couldn't process this object.",
      $null, # error ID
      [System.Management.Automation.ErrorCategory]::InvalidData, # error category
      $null) # offending object
    )
}

# PSv4-, using New-Object:
& { [CmdletBinding()] param()  # quick mock-up of an advanced function

  $PSCmdlet.WriteError((
    New-Object System.Management.Automation.ErrorRecord "Couldn't process this object.",
      $null,
      ([System.Management.Automation.ErrorCategory]::InvalidData),
      $null
  ))

}

Example use of $PSCmdlet.ThrowTerminatingError() to create a statement-terminating error:

# PSv5+, using the static ::new() method to call the constructor.
# The specific error message and error category are sample values.
& { [CmdletBinding()] param()  # quick mock-up of an advanced function

  $PSCmdlet.ThrowTerminatingError(
    [System.Management.Automation.ErrorRecord]::new(
      # The underlying .NET exception: if you pass a string, as here,
      #  PS creates a [System.Exception] instance.
      "Something went wrong; cannot continue pipeline",
      $null, # error ID
      [System.Management.Automation.ErrorCategory]::InvalidData, # error category
      $null  # offending object
    )
  )

}

# PSv4-, using New-Object:
& { [CmdletBinding()] param()  # quick mock-up of an advanced function

  $PSCmdlet.ThrowTerminatingError((
    New-Object System.Management.Automation.ErrorRecord "Something went wrong; cannot continue pipeline",
      $null, # a custom error ID (string)
      ([System.Management.Automation.ErrorCategory]::InvalidData), # the PS error category
      $null # the target object (what object the error relates to)
  ))

}
SteveL-MSFT commented 7 years ago

@juanpablojofre this would make a great about topic

zjalexander commented 7 years ago

https://github.com/PowerShell/PowerShell/issues/3629

nightroman commented 6 years ago

A note about ThrowTerminatingError, either regression from v2 or some not clear design change.

In the following script, ThrowTerminatingError is not caught in v5 and v6. It is caught in v2.

[CmdletBinding()]
param()

$caught = 'not-caught'
try {
    # this is not caught in v5 (works in v2)
    $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ([Exception]'some-error'), $null, 0, $null))
}
catch {
    $caught = 'was-caught'
    throw
}
finally {
    $caught
    'in-finally'
}

Copied from https://github.com/nightroman/PowerShellTraps/tree/master/Basic/ThrowTerminatingError/Catch-is-not-called

strawgate commented 6 years ago

@mklement0 I see your recommendation to utilize $PSCmdlet.WriteError() instead of write-error to fix the issue with $? but it looks like these have different behaviors when the "ErrorActionPreference" of the script/module is set to "stop".

Im testing this in Powershell 5.1

If erroractionpreference = stop and you use $PSCmdlet.WriteError() it appears to produce a locally non-terminating error but when it leaves the function it appears to be terminating. That error is not catch-able in the function producing the error but is catche-able by the caller.

If erroractionpreference = stop and you use write-error it appears to produce a statement terminating error which can be caught by the function.

$ErrorActionPreference="stop"
$VerbosePreference="continue"

function Test-WriteError {
    [CmdletBinding()]
    Param( )
    try {
        $errorRecord = New-Object Management.Automation.ErrorRecord (([Exception]'some-error'), $null, 0, $null)
        $PSCmdlet.WriteError($errorRecord) # This does not get caught when erroraction is set to stop
    } catch {
        write-verbose "Caught"
    }
}

function Test-Write-Error {
    [CmdletBinding()]
    Param( )
    try {
        write-error "Test" #This gets caught if ErrorActionPreference is Stop or -erroraction Stop is passed to this command
    } catch {
        write-verbose "Caught"
    }
}

Running Test-WriteError will terminate the script:

PS > test-writeerror; write-host "print"
some-error
At line:1 char:1
+ test-writeerror; write-host "print"
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Test-WriteError], Exception
    + FullyQualifiedErrorId : Test-WriteError

Running Test-Write-Error will not terminate the script:

PS > test-write-error; write-host "print"
VERBOSE: Caught
print

In other words, $PSCmdlet.WriteError does not appear to throw a terminating error when $ErrorActionPreference = Stop whereas write-error does

mklement0 commented 6 years ago

@strawgate: Thanks for pointing out that difference - I have no explanation for it.

To me, your discovery suggests that one currently should always use $PSCmdlet.WriteError() instead of Write-Error (which, needless to say, is very unfortunate):

While you could argue that technically Write-Error exhibits the correct behavior - with Stop in effect, writing a non-terminating error should throw an exception - pragmatically speaking, the $PSCmdlet.WriteError() behavior is much more sensible:

You don't want to catch your own attempts with try / catch inside an advanced function to report a non-terminating error. Instead, you want the caller to handle this, as implied by the calling context's $ErrorActionPreference value or the -ErrorAction value passed to the advanced function (which function-internally is translated into a local-scope $ErrorActionPreference variable reflecting that value).

There is no good solution to this problem with Write-Error:

alx9r commented 6 years ago

@mklement0 Thank you very much for your excellent post. It has been invaluable to me for making sense of a number of things.

Script Terminating Errors vs Exceptions

Is there a difference between "script-terminating errors" and exceptions? There seems to be places in the PowerShell project that distinguish between "script-terminating errors" and exceptions. I haven't, however, been able to observe a difference between them. Is there a difference?

What's the "Script" in "Script-Terminating Error"?

What is "script" meant to refer to in "script-terminating errors"? It seems like "script-terminating errors" often (almost always in my use of PowerShell) do something other than terminate a script. It seems like the only case where the stack unwinding caused by a "script-terminating error" stops at something that would be called a "script" is where there happens to be nothing that catches the "script-terminating error". Am I missing something? Is there some other definition of "script" that applies to "Script-Terminating Error"?

.ThrowTerminatingError does not a Statement-Terminating Error Make

The original post includes the following statements:

Statement-terminating errors truly only terminate the statement at hand. By default, the enclosing scripts continues to run. ... PowerShell has NO keyword- or cmdlet-based mechanism for generating a statement-terminating error. The workaround - available in advanced functions only - is to use $PSCmdlet.ThrowTerminatingError():

Consider, however, that

function e {
    try { 1 }
    catch{ Write-Host 'catch upstream' }
}

function f {
    param ( [Parameter(ValueFromPipeline)]$x )
    process {
        # nonexistentcommand
        $PSCmdlet.ThrowTerminatingError(
            [System.Management.Automation.ErrorRecord]::new(
                'exception message',
                'errorId',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $null
            )
        )
    }
}

try
{
    e | f
    Write-Host 'statement following statement with .ThrowTerminatingError'
}
catch
{
    Write-Host 'outer catch'
}

outputs outer catch. If it were true that "statement-terminating errors truly only terminate the statement at hand" and .ThrowTerminatingError() caused a statement-terminating error, it seems to me that the output would have been statement following statement with .ThrowTerminatingError.

mklement0 commented 6 years ago

@alx9r: Thanks for the nice feedback and digging deeper. I'll need more time to give your post the attention it deserves, but let me say up front that it was I who came up with the term "script-terminating", out of necessity, given that the existing docs made no distinction between the sub-types of terminating errors.

Thus, there's nothing authoritative about this term, and if it turns out to be a misnomer, we should change it.

mklement0 commented 6 years ago

@alx9r:

Disclaimer: Little of what I state below has been arrived at through source-code analysis. All readers are welcome to point out any misinformation.

Is there a difference between "script-terminating errors" and exceptions?

From what I understand, all PowerShell errors (as opposed to stderr output from external utilities) are exceptions under the hood. A script-terminating error is simply an exception that isn't caught by PowerShell.

What's the "Script" in "Script-Terminating Error"?

Presumably, the more technically accurate term would be runspace-terminating error, but the term runspace is not a term a PowerShell(-only) user is necessarily expected to be familiar with. [Update: It's hard to come up with a succinct, technically accurate term, though calling such errors fatal is a pragmatic alternative - for the technical underpinnings, see https://github.com/PowerShell/PowerShell/issues/14819#issuecomment-786121832]

In practice, what I call a script-terminating error, when uncaught:

Again, the most descriptive term is open to debate. Session-terminating error is perhaps an alternative, but that also could be confusing with respect to behavior at the command prompt.

If it were true that "statement-terminating errors truly only terminate the statement at hand" and .ThrowTerminatingError() caused a statement-terminating error, it seems to me that the output would have been statement following statement with .ThrowTerminatingError.

In this context, the pipeline as a whole is the statement, even though a pipeline is itself by definition composed of sub-statements (an expression or command as the 1st segment, and commands as the subsequent segments).

Again, the terminology is open to debate: If an actual pipeline is involved, you could argue that pipeline-terminating error is the better choice, but note that that doesn't apply to expression-only statements such as 1 / 0 that also generate a statement-level-only error - it is for that reason that I stuck with statement as the qualifier.

Another example to demonstrate the pipeline behavior of a statement-terminating error:

# Advanced function that generates a statement-(pipeline-)terminating error
# when the input object is integer 2
function f {
  param ([Parameter(ValueFromPipeline)] [int] $i)
  begin { write-host 'before2' }
  process {
    if ($i -eq 2) {
      $PSCmdlet.ThrowTerminatingError(
        [System.Management.Automation.ErrorRecord]::new(
            'exception message',
            'errorId',
            [System.Management.Automation.ErrorCategory]::InvalidOperation,
            $null
        )
      )
    }
    $i
  }
  end { write-host 'after2' }
}

# Send an 3-element array through the pipeline whose 2nd element triggers the statement-terminating error.
1, 2, 3 | % { write-host 'before1' } { $_ } { write-host 'after1' } | f

The above yields:

before1
before2
1
f : exception message
...

That is, the entire pipeline was instantly terminated when the statement-terminating error was generated (no further input objects were processed, and the end blocks didn't run).

alx9r commented 6 years ago

Since I came across this post months ago I've been trying to arrive at empirical proof of the various statements in the OP about "terminating", "non-terminating", "script-terminating", and "statement-terminating" errors. Currently I am doubtful that there exists definitions of those terms that would result in a useful taxonomy of PowerShell errors.

I have found few useful generalizations about PowerShell errors that stand up to testing. Most hypotheses involving generalizations and nuance (including some in OP) can be disproven with a small amount of testing. The truth about PowerShell errors seems remarkably resistant to simplicity.

I am fairly confident, however, that the following generalizations about PowerShell errors are true:

  1. A PowerShell expression or statement might do all, some subset, or none of the following error-related actions: a. write records to the $global:Error variable b. write records to a non-global copy of $Error c. write records to a variable named in an -ErrorVariable argument d. output to PowerShell's error stream e. update the value of $? f. terminate a pipeline g. terminate a statement h. throw an exception
  2. Whether each of the actions in (1) occurs depends at least on one or more of the following: a. the value of $ErrorActionPreference b. the value passed to -ErrorAction or the value of $ErrorAction c. the presence of an enclosing try{} block (eg. PowerShell/PowerShell#6098) d. the precise way an error is reported (eg. Write-Error vs. $PSCmdlet.WriteError() vs. throw $PSCmdlet.ThrowTerminatingError())

More nuanced generalizations would, of course, be useful. Currently, however, I am skeptical that such generalizations can be found and proven. Indeed my attempts at doing so have seemed sisyphean.

From my perspective the useful takeaway from this exercise is currently the following:

mklement0 commented 6 years ago

@alx9r:

My writing the OP had multiple intents:

alx9r commented 6 years ago

@mklement0 I think we are on the same page in striving for a better record of PowerShell error behavior.

I wanted to provide a pragmatic summary of the de-facto behavior, so as to help us poor souls who have to live with and make some sense of the current behavior.

As far as I'm concerned your OP succeeds at this. And I am very thankful that you wrote it.

While I don't doubt that my summary doesn't cover all nuances - it was arrived at empirically as well, not through studying source code - my hope was that it covers most aspects and gets at least the fundamentals right.

I think your OP is probably as close to "fundamentally" correct as I've seen. The problem is that there are so many exceptions and nuances to the "fundamental" behavior that "knowing" the "fundamentals" is not particularly useful.

...if you have pointers as to where my summary is fundamentally off, please share them.

The truth of PowerShell errors is sufficiently messy that I am not convinced that there are objective fundamentals that your summary could be "off" from.

I think it is probably useful for me to continue to report repros of newly-surprising error handling behavior. I'm not sure what the best venue for that is, but when I encounter behavior that is sufficiently inscrutable, I expect to continue reporting it to the PowerShell/PowerShell repository.

I wanted to start a conversion about cleaning up the byzantine mess that PowerShell error handling currently is..

It seems to me that making PowerShell errors less byzantine would involve numerous breaking changes. I don't think that would be a good way forward. It seems like we are mostly stuck with the current PowerShell error behavior.

mklement0 commented 6 years ago

@alx9r:

I don't think that would be a good way forward. It seems like we are mostly stuck with the current PowerShell error behavior.

It certainly wouldn't be a good way forward in Windows PowerShell for reasons of backward compatibility, but I'm still holding out hope for PowerShell Core... (the official releases so far are an unhappy mix of making long-time Windows users unhappy due to breaking changes while carrying forward enough warts to make long-time Unix users stay away, if I were to guess).

iSazonov commented 6 years ago

It seems we need add statement/expression/pipeline definitions.

mklement0 commented 6 years ago

@iSazonov:

Indeed. Let me take a stab at it (again, not arrived at by studying the source code - do tell me where I'm wrong):

In the context of a single statement, (...) can be used to compose a statement from the two subtypes; aside from enforcing precedence in an expression (e.g., (1 + 2) * 3), (...):

By contrast, operators & {...}/ . {...} and $(...) / @(...) allow you to nest statements, i.e., embed one - or more - statements in a statement, and all embedded statements are statements in their own right with respect to statement-terminating errors.
More generally, a script block passed to another command for execution (e.g., ForEach-Object) is its own statement context(s).
The same applies to the script blocks that make up group statements (higher-order statements), by which I mean keyword-based flow-control constructs, such as loops (foreach/for, do, while), conditionals (if, switch) and exception handlers (try ... catch ... finally, trap); by contrast, a statement-terminating error in the conditional of such a group statement does terminate the entire construct (e.g., if (1/0) { 'yes' } else { 'no' }).

To illustrate the difference between using (...) to form a part of the enclosing statement and a nested statement with respect to statement-terminating errors (using $(...) in the example, but it applies equally to @(...) and analogously to . { ... } / & { ... }):

iSazonov commented 6 years ago

We could use Language specification for Windows PowerShell 3.0

alx9r commented 6 years ago

Situations in which statement-terminating errors occur:

  • PowerShell's fundamental inability to even invoke a command generates a statement-terminating runtime error, namely: ...
    • A cmdlet or function call with invalid syntax, such as incorrect parameter names or missing or mistyped parameter values: Get-Item -Foo # -Foo is an invalid parameter name

This is consistent with my experiments, however, there is more nuance to this as follows:

Here is a repro of the terminating and non-terminating errors cause by failed command-line and pipeline parameter binding, respectively:

$ErrorActionPreference = 'Continue'
function f {
    param ( [Parameter(ValueFromPipeline,Mandatory)][int]$x )
    process { Write-Host $x }
}

Write-Host '=== Command-line binding error'

f -x 'not an int'
Write-Host 'That caused a command-line binding error causes a statement-terminating error...'

try
{
    f -x 'not an int'
}
catch
{
    Write-Host '...which is caught by a catch block.'
}

Write-Host '=== Pipeline binding error'

try
{
    1,2,'not an int',3 | f
    Write-Host 'That was a pipeline-binding error, which was non-terminating.'
}
catch
{
    Write-Host 'This catch block is not run because the pipeline binding error is non-terminating'
}
mklement0 commented 6 years ago

Good catch (accidental pun), @alx9r. I've update the OP, which now links to your comment.

pstephenson02 commented 5 years ago

This is the best reference for PowerShell error handling I've come across on the web. Would love to see more formal documentation with this type of info! Thanks!

jtmoree-github-com commented 5 years ago

Seems like we are missing an opportunity to fix the error handling mess as part of a move to Powershell Core (6) .... There will likely never be another major shift in powershell which changes the EXE name and much of the underlying structure.

mklement0 commented 5 years ago

I agree that an opportunity was missed, but please see the discussion in Is it time for "PowerShell vZeroTechnicalDebt" and/or for an opt-in mechanism into new/fixed features that break backward compatibility?

AndersRask commented 5 years ago

Thanx for the insightfull article. I can confirm that in Azure Runbooks there is indeed a difference between throw and $PSCmdlet.ThrowTerminatingError($_) in line with your distinction between script and statement terminating error. In runbooks you would always distinguish non-terminating and terminating errors as Stream Errors and Exceptions:

however if you (as i have seen many runbook authors do) use try/catch pattern with ThrowTerminatingError() inside an adv function, this will NOT trigger the runbook to have a failed state (wich one would rightfully expect as the name hints). This is of course because the script is not terminated, only the adv function you called:

`function Test-ExceptionHandling { [cmdletbinding()] param () process { try { Write-Output "Process start try" throw "this is terminating.... !" Write-Output "Process end try" } catch { Write-Output "Process start catch" $PSCmdlet.ThrowTerminatingError($_) # does NOT throw a terminating error! Write-Output "Process end catch" } } }

Test-ExceptionHandling`

If you replace $PSCmdlet.ThrowTerminatingError($) with throw $ the runbook will fail

Kriegel commented 5 years ago

@mklement0 I even thank you very much for your excellent post. @all thank you for the high quality discussion.

I came here on my research of PowerShell event Logging (especially Log to text file) Logging and PowerShell is a very sad story ...

So I miss Words about the use and pitfalls of -ErrorVariable

You have showed up many quirks and byzantine things with PowerShell Error handling.

So many PowerShell Users may be, very confused how to set up an Error handling in PowerShell that has an reliable expected behavior.

So how to give users a guardrail for error handling?

In short,

In the past I gave my students the advice to allways use Advanced Functions and inside them to use the following construct in the Process Block (similar to be used in Begin{} and End{}).

The following example of an advanced Function block worked for me in the past.

Notice: A Try{} block creates NO new scope, the $ErrorActionPreference = ... statement inside a Try{} block would NOT be confined to the Try{} block !

    Process {

        ForEach ($Item in $InputObject) {

            Try {
                $ErrorActionPreference = 'Stop'

                # Example to call a cmdlet 
                Get-Item -Path 'C:\nosuch'

                # Code here
                 # Code here
                  # Code here
                   # ...

            } Catch {
                # do error handling here

                # possibly re-throw the Error
                Write-Error $Error[0]  # -ErrorAction 'Stop'

            } # Finally {}
        }
    }

So the question with this code is What to use and what not?

The following assumption is inncorect

~~Using $PSCmdlet.WriteError($) ? Really? What if the $ Variable contains an Exception and not an $ErrorRecord? This will fail.~~ So i Stay with Write-Error

or to make an cumbersome switch inside the Catch block?

Warning! Incorrect code snippet here!

Catch {
    If($_ -is [System.Management.Automation.ErrorRecord]) {
        $PSCmdlet.WriteError($_)
    } Else {
        $PSCmdlet.WriteError((New-Object System.Management.Automation.ErrorRecord -ArgumentList $_))
    }
}

The next full blown code example is only for Discussion purposes

Function Get-FunctionTemplate {

    [CmdletBinding()]
    Param(
        [Parameter()]
        [Object[]]$InputObject
    )

    Begin {

       $ErrorActionPreference = 'Stop'

        Try {

            # Code here
             # Code here
              # Code here
               # ...

        } Catch {

            $PSCmdlet.WriteError($_)

        } # Finally {}
    }

    Process {
         $ErrorActionPreference = 'Stop'

        ForEach ($Item in $InputObject) {

            Try {
                # Example to call a cmdlet with paranoic (double) Stop on Error
                Get-Item -Path 'C:\nosuch' -ErrorAction Stop

                # Code here
                 # Code here
                  # Code here
                   # ...

            } Catch {

                $PSCmdlet.WriteError($_)

            } # Finally {}
        }
    }

   End {

        Try {

            # Code here
             # Code here
              # Code here
               # ...

        } Catch {

            # Retrow the Error with different commands

            $PSCmdlet.WriteError($Error[0])
            # or
            Write-Error $Error[0]
            # or
            $PSCmdlet.WriteError($_)
            #or
            Write-Error $_

            #or
            Throw $Error[0]
            #or 
            Throw # $_

        } # Finally {}
    }
}
iSazonov commented 5 years ago

I came here on my research of PowerShell event Logging (especially Log to text file) Logging and PowerShell is a very sad story ...

@Kriegel Welcome to PowerShell Core repo https://github.com/PowerShell/PowerShell to share and discuss your experience, proposals and thoughts.

Kriegel commented 5 years ago

@iSazonov I do not understand why you pointing me to the PowerShell Repo. I am fully aware that I am here in the Docs section and the topic is

about_Error_Handling

Was my post not in that topic?

iSazonov commented 5 years ago

@Kriegel My welcome is not related to the topic. I see you have great experience and we would appreciate your feedback in PowerShell repo too.

Kriegel commented 5 years ago

@iSazonov O now i see! Thank you very much, for the welcome and your kind words to me! Back to topic... :-)

Kriegel commented 5 years ago

I found 2 Documents that can help us with this topic. We can link or reuse the content of those writings:

PowerShell Practice and Style, Best-Practices, Error-Handling https://github.com/PoshCode/PowerShellPracticeAndStyle/blob/master/Best-Practices/Error-Handling.md

An Introduction to Error Handling in PowerShell https://gist.github.com/TravisEz13/9bb811c63b88501f3beec803040a9996

mklement0 commented 5 years ago

@Kriegel: Glad to hear you found the OP useful, and thanks for your input.

Just a few quick responses :

inside directly after the Try { put the Statement $ErrorActionPreference = 'Stop' to make this work in the Try scope

The try compound statement actually does not create a new scope, so whatever you assign to $ErrorActionPreference in the try block remains in effect after the try statement.

inside the Catch {} use Write-Error $Error[0] to re-throw the Error.

Generally, Write-Error only respects the caller's preference if the caller is in the same scope domain as the callee - the problem discussed in https://github.com/PowerShell/PowerShell/issues/4568

Specifically, your try block $ErrorActionPreference = 'Stop' statement effectively makes the Write-Error always a script-terminating error.

i heard rumor that $_ can have an unexpected Value inside a Catch block.

I'm not personally aware of such cases, but do tell us if you know specifics.

Using $PSCmdlet.WriteError($) ? Really?

I agree that you shouldn't have to use $PSCmdlet.WriteError(), but the problem with Write-Error is that it doesn't set $? to $false in the caller's scope.

Then again, the usefulness of $? is currently severely compromised anyway - see https://github.com/PowerShell/PowerShell/issues/3359, which is now starting to become more of an issue in the RFC for implementing Bash-style && and || operators.

Kriegel commented 5 years ago

@mklement0 thank you for your response. Because I had read ALL cross references out of this discussion thread I am aware of most of your facts inside your response.

In one Point you I think you did not catch my intention. I mean $PSCmdlet.WriteError() is a bad candidate to rethrow, because it has no overload that accepts an .NET System.Exception Type. See #10735

mklement0 commented 5 years ago

@Kriegel

I am aware of most of your facts inside your response.

I see. That wasn't obvious to me, since in your original comment you hadn't indicated awareness of operating based on mistaken assumptions, such as thinking that a try block creates a new scope, thinking that an $ErrorActionPreference = ... statement in there would be confined to it.

Perhaps you can edit a warning into your original comment.


As for $PSCmdlet.WriteError() / https://github.com/PowerShell/PowerShell/issues/10735: I think @vexx32 is correct there:

In a catch block - as far as I know - $_ is always a [System.Management.Automation.ErrorRecord] instance that you can directly pass to Write-Error / $PSCmdlet.WriteError().

However, note that with Write-Error you shouldn't just use Write-Error $_, because that will bind the error record to the -Message parameter (at least currently, unfortunately - see https://github.com/PowerShell/PowerShell/issues/10739), which converts it to a string that preserves the original exception's message only, wrapped in a [Microsoft.PowerShell.Commands.WriteErrorException] exception.

Instead, use Write-Error -ErrorRecord $_ (or $PSCmdlet.WriteError($__) if setting $? to $false in the caller's scope matters to you).


P.S.: Should you ever find yourself having to write an exception directly to the error stream, Write-Error actually does support that, via the -Exception parameter (e.g.: Write-Error -Exception ([System.InvalidOperationException]::new()) ...

alx9r commented 4 years ago

I encountered another inconsistency that I think has not yet been taxonomized here. @mklement0 could you take a look and see whether this is already covered in your OP somehow?


catch{} invocation on "script-terminating error" depends on pipeline particulars

Per the following repro a script-terminating error encountered in a try{} does not cause invocation of the catch{} when the error occurs downstream in the pipeline. The following table shows that the behavior in that particular condition is anomalous compared with the behavior of all of the nearly-identical error-handling conditions tested in that repro.

error kind location catch invoked finally invoked
throw in situ yes yes
throw downstream yes yes
stopping in situ yes yes
stopping downstream no yes

Repro

function f {
    param([ValidateSet('throw','stopping_error','none')]$Failure)
    Write-Host "`r`nf -Failure $Failure"
    try 
    {
        switch ($Failure)
        {
            'throw'          { throw 'something'}
            'stopping_error' {  Get-Item bogus -ea Stop }        
        }
        Write-Host 'end of try'
    }
    catch   { Write-Host 'catch'}
    finally { Write-Host 'finally'}    
}

function g {
    param([ValidateSet('throw','stopping_error','none')]$Failure)
    Write-Host "`r`ng -Failure $Failure"
    & {
        try {
            'output'
            Write-Host 'end of try'
        }
        catch   { Write-Host 'catch'}
        finally { Write-Host 'finally'}
    } | 
    % {
        switch ($Failure)
        {
            'throw'          { throw 'something'}
            'stopping_error' {  Get-Item bogus -ea Stop }
        }
    }
}

f -Failure none
f -Failure throw
f -Failure stopping_error

g -Failure none
g -Failure throw
g -Failure stopping_error

outputs

f -Failure none
end of try
finally

f -Failure throw
catch
finally

f -Failure stopping_error
catch
finally

g -Failure none
end of try
finally

g -Failure throw
catch
finally

g -Failure stopping_error
finally
Get-Item : Cannot find path ...
...
+             'stopping_error' {  Get-Item bogus -ea Stop }
+                                 ~~~~~~~~~~~~~~~~~~~~~~~
mklement0 commented 4 years ago

That's a nice repro, @alx9r - and it isn't yet covered in the OP. It might surprise people that the catch inside the script block fires at all in theg() case, but I guess it makes sense technically, since the whole pipeline is a single statement.

My guess as to why the catch doesn't fire with -ea Stop is that the promotion to a script-terminating error via -ea Stop happens at the level of the whole statement, meaning that the statement's caller is responsibly for catching it - and, indeed, wrapping the g call as a whole in try / catch works.

Perhaps @rjmholt can shed more light on this.

binyomen commented 4 years ago

I was spending a lot of time trying to figure out how PowerShell error handling works, and finding this probably saved me weeks of time. Thanks!!!

I was having a lot of trouble finding a good central place for all the quirks of PowerShell in general, especially how they differ from version to version. And some places seemed to have conflicting information.

All the examples posted in this thread inspired me to make a site where PowerShell behavior is documented by automatically running PowerShell and displaying the result! I ended up whipping up https://github.com/benweedon/pwsh-live-doc/. I've added a few examples to it so far, and will keep adding more in the future as I learn about them. If you have anything to add feel free to file an issue or open up a PR :)

I hope you can find this useful as an archive for PowerShell behavior, or at least that you can use it to do tests of your own :)

Kriegel commented 4 years ago

@mklement0 I am very thankful to point me to the -ErrorRecord param of Write-Error. I did not remark that Write-Error takes only the Exception without it.

Sadly the InvocationInfo is Readonly So i sometimes create my own ErrorRecords to 'Fake' the InvokationInfo and other Properties.

Inside the following Example, the reported InvokationInfo source of the Error is Get-Item and not the surrounding Function. In the Example I like to report Get-Err as the Originator and not Get-Item

Function Get-Err {

    Try {
    Get-Item 'C:\no\exist' -ErrorAction 'Stop'
    } Catch {
        Write-Error -ErrorRecord $Error[0]
    }

}

Get-Err

There is an article on PowerShell Magazine to show how to create custom Errors. https://www.powershellmagazine.com/2011/09/14/custom-errors/

even see Feature Request here: https://github.com/PowerShell/PowerShell/issues/3190

So I think he had even the need to create custom Errors ;-) Custom Errors can even be used to wrap .NET Class Exceptions with the appropriate Meta Information's. Write-Error serves this scenario not very well ....

The Code out of this 'PowerShell Magazine' article is not read able, I have attached it. New-ErrorRecord.txt

@benweedon

I was having a lot of trouble finding a good central place for all the quirks of PowerShell in general,

A good point to start is here: ;-) https://github.com/nightroman/PowerShellTraps And the issues in the main Powershell repo

vexx32 commented 4 years ago

@Kriegel if you want to change that, Write-Error is not the best tool for it.

Instead, mark the function with [CmdletBinding()] like so:

function Get-Err {
    [CmdletBinding()]
    param()

    # function code
}

And from there, you can use $PSCmdlet.WriteError($errorRecord) and it will automatically override the invocationinfo to point at the function which called WriteError() rather than the original error source. 🙂

Kriegel commented 4 years ago

@vexx32 I like that $PSCmdlet.WriteError($errorRecord) automatically overrides the invocationinfo. I some one dislike it, then we are here: https://github.com/PowerShell/PowerShell/issues/10733 (LOL)

johndog commented 3 years ago

Strict modes don't seem to have been discussed, but they introduce yet another nuance into the domain.

Here's one dichotomy...

First, context:

> $ErrorActionPreference = "Continue"
> Set-StrictMode -Version Latest

Then this shows that using an undefined variable generates an error but doesn't stop execution:

> & {$nonExistantVariable;"got here"}

InvalidOperation: The variable '$nonExistantVariable' cannot be retrieved because it has not been set.
got here

But the same script block run with Invoke() generates an exception, not just an error:

> {$nonExistantVariable;"got here"}.Invoke()

MethodInvocationException: Exception calling "Invoke" with "0" argument(s): "The variable '$nonExistantVariable' cannot be retrieved because it has not been set."

It would have been much nicer of course if strict mode always produced an exception, regardless of ErrorActionPreference.

mklement0 commented 3 years ago

@johndog, good idea to cover strict-mode violations too; I've just updated the initial post.

The short of it is that strict-mode violations result in statement-terminating errors by default.

This applies to both your examples, which only differ in the scope of what constitutes the statement being terminated:

Note that while PowerShell's error handling is built on .NET exceptions (in the case of the two flavors of terminating errors), it's better to discuss it in terms of non-terminating vs. statement-terminating vs. script-terminating errors.

davidtrevor23 commented 1 year ago

Trying to wrap my head around pipeline execution and statement-terminating behavior. From all the posts and comments, I gathered the understanding that the following two methods can both be used to provoke a statement-terminating behavior.

But in my tests they do not behave the same. The first example continues the pipeline, whereas the second terminates the pipeline. Can someone explain where my understanding is wrong?

function calc {
    param( [Parameter(ValueFromPipeline=$true)]$i )
    PROCESS { 1 / $i }
}
-1,0,1 | calc | Write-Output

-1 RuntimeException: Attempted to divide by zero. 1

function calc {
    param( [Parameter(ValueFromPipeline=$true)]$i )
    PROCESS { if ($i -ne 0) { 1 / $i } else { $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new('exception message','errorId',[System.Management.Automation.ErrorCategory]::InvalidOperation,$null)) } }
}
-1,0,1 | calc | Write-Output

-1 RuntimeException: Attempted to divide by zero.

On another note, can someone point me to a resource that explains how exactly objects get processed when passed through the pipeline operator (in what order). I do not quite understand the order of operation in this comment: https://github.com/MicrosoftDocs/PowerShell-Docs/issues/1583#issuecomment-366479224

mklement0 commented 1 year ago

Thus,1 / 0 in PROCESS { 1 / $i } terminates just 1 / $i and continues execution (even inside the script block; placing something after would still execute, e.g. PROCESS { 1 / $i ; 'after' }

A better comparison of the two scenarios would be:

# First statement on each line prints error only; 'after' still prints.
1, (1 / 0),    2 | Write-Output; 'after'
1, (0 | calc), 2 | Write-Output; 'after'

Now both errors terminate the entire statement (which happens to be a pipeline in this example, but it equally applies to an expression) and there is no success output. However the next statement ('after') executes.

Perhaps the following addresses your second question:

Note that something like 1, (1 / 0), 2 is an array-construction expression, which means that all elements are evaluated up front. The same applies to a pipeline or expression enclosed in (...) Thus, the statement-terminating error occurs right away and terminates the entire pipeline.

By contrast, if you use $(...), @(...) or & { ... } with separate statements (separated with ; or just newlines), it is again just the statement that triggers the error alone that is terminated:

# First statement on each line prints 1, then error, then 2; 'after' still prints.
& { 1; (1 / 0); 2 }    | Write-Output; 'after'
& { 1; (0 | calc); 2 } | Write-Output; 'after'

1 and 2 get to print, because they're separate statements; (1 / 0) and (0 | calc) only terminate themselves. Note: With $(...) and @(...) the error message prints first, because these two operators collect all success output first, whereas & { ... } streams its output.

denis-komarov commented 1 month ago

@mklement0 Thank you for such a fundamental and in-depth description of error handling mechanisms. Just for the sake of completeness, it seems that another separate class of errors could be mentioned. This is "Fatal error. Internal CLR error" against which even try catch is powerless. However, I am not sure of the practical value of this addition.

alx9r commented 5 days ago

Origin Location of Errors from Explicitly-Invoked Scriptblocks

The resulting error objects that arise in a PowerShell catch{} block contain location information for the actual source of the error that is, at best, elusive. The contents of the error object are affected by at least the following:

Consider files containing scriptblocks according to the following table:

Script File Name Scriptblock
divide by zero.ps1 { 1/0 }
error_action_stop.ps1 {$PSCommandPath \| Get-Item \| % BaseName \| Write-Error -ErrorAction Stop}
throw.ps1 {throw $($PSCommandPath \| Get-Item \| % BaseName)}

By invoking each of those scriptblocks using one of the calling methods inside a try{}, the resultant error object can be caught and examined. The object tree of each such an error object can be traversed to find any mentions of the file where the scriptblock is defined. The paths of possible mentions in these object trees is numerous, and the prevalence of most such mentions across the matrix of call methods and error causes is sparse. There are, however, four paths that together contain mention of the originating scriptblock for all of these combinations of error and calling method. Those paths are summarized in the following table:

Mentioned In
Error Cause Method $.
ScriptStackTrace
$.
InvocationInfo.
ScriptName
$.
Exception.
InnerException.
ErrorRecord.
ScriptStackTrace
$.
Exception.
ErrorRecord.
InvocationInfo.
ScriptName
divide by zero call_operator
divide by zero dot_source_operator
divide by zero ForEach-Object
error_action_stop call_operator
error_action_stop dot_source_operator
error_action_stop ForEach-Object
throw call_operator
throw dot_source_operator
throw ForEach-Object
divide by zero Scriptblock.Invoke
error_action_stop Scriptblock.Invoke
throw Scriptblock.Invoke

This suggests the following: