Powershell: How you re-throw changes our ability to catch the exception:

Created on 21 Feb 2018  路  9Comments  路  Source: PowerShell/PowerShell

Exception Handling in PowerShell is the pits (part 2)

I am _re-filing_ this from Connect from as it appears to have gone missing again.

Re-throwing exceptions with throw (rather than throw $_) changes how exception handling works -- apparently breaking our ability to catch the outer exception and still handle the inner exception _type_ if it's thrown unwrapped.

Steps to reproduce

Given an example case where the .Net Framework or PowerShell throws an exception with an InnerException (the actual types aren't important, just the fact that they are nested).

function Throw-InnerException {
       $infe = [System.Management.Automation.ItemNotFoundException]::new("File Not Found")
       $pbe =  [System.Management.Automation.ParameterBindingException]::new( "FileName", $infe)
       throw $pbe
}

If we write code that calls that function, and then rethrows any exceptions, we do not expect that to change the exception -- but it has the effect of changing how outer exception handling works.

Note that the community believes wrapping things in a try/catch+rethrow is, in general, a _good_ practice, since it ensures that you (and any code you're calling) don't inadvertently turn terminating exceptions into non-terminating ones.

Here's a simple example:

function Invoke-Correctly {
    try {
        <# Do something #>
        # Call the cmdlet which might throw
        Throw-InnerException
        <# Do other things if it does not throw #>
        # Catch and re-throw, so callers can handle any unhandled errors
    } catch { throw }
}

Now, when that function is called (and _if you try to handle both exception types_), the inner one is incorrectly caught, even if it's handler is defined last (this will incorrectly output INFE):

$Error.Clear()
try {
    Invoke-Correctly
} catch [System.Management.Automation.ParameterBindingException] {
    "PBE" 
} catch [System.Management.Automation.ItemNotFoundException] {
    "INFE"
}

Only if you do not have a handler for the _inner exception_, can you catch the outer exception. This will correctly output PBE and INNER: ItemNotFoundException:

$Error.Clear()
try {
    Invoke-Correctly
} catch [System.Management.Automation.ParameterBindingException] {
    "PBE"
     # Note that the "ItemNotFoundException" is inside this one:
    "INNER: "+ $_.Exception.InnerException.GetType().Name 
} catch {
    "INFE (won't be caught)"
}

The weird thing is that we can fix this. We can get the catching behavior we _expected_ by simply changing the _rethrow_ to throw $_ -- just adding the $_ to the throw statement results in what is caught changing. This _should not_ happen:

function Invoke-Better {
    try {
        # Do something
        # Call the cmdlet which might throw
        Throw-InnerException
        # Do other things if it does not throw
        # Catch and re-throw, so callers can handle the underlying error
    } catch { throw $_ }
}

Expected behavior

We should be able to catch the outer exception even if we have a handler for the inner exception. Note that this will work, if we call Invoke-Better (but doesn't work if we call Invoke-Correctly):

$Error.Clear()
try {
    Invoke-Better
} catch [System.Management.Automation.ParameterBindingException] {
    "PBE" 
} catch [System.Management.Automation.ItemNotFoundException] {
    "INFE"
}

Actual behavior

Using throw $_ as in Invoke-Better works the way we expect it too.

But using throw as in Invoke-Correctly (which is supposed to mean the same thing), hinders our ability to handle the exception correctly.

Environment data

This bug has always existed (or at least, has existed for a very long time), and it still persists in PowerShell 6.

> $PSVersionTable
Name                           Value
----                           -----
PSVersion                      6.0.0
PSEdition                      Core
GitCommitId                    v6.0.0
OS                             Microsoft Windows 10.0.15063
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
Committee-Reviewed Issue-Discussion WG-Language

Most helpful comment

Recently I found myself digging into this issue while troubleshooting something related that I was working on in the debugger and compiler, and there are actually a few specific issues identified above that need to be discussed separately, as follows:

Issue descriptions

  1. Exceptions derived from System.Management.Automation.RuntimeException that contain inner exceptions cannot be caught by the outer exception type if the thing that was thrown was the raw exception (as opposed to an ErrorRecord or a wrapping RuntimeException). They can, however, be caught using the inner exception type (which should never be possible, if PowerShell aligns itself to similar designs in other languages like C#). This is an issue in lines 1464-1466 of the FindMatchingHandler static method, which should create a new ErrorRecord that wraps the RuntimeException and assign those two variables differently if the parent (the exception in the rte variable itself) contains the exception, as can be seen by inspecting the ErrorRecord property on the RuntimeException. Additionally a minor logic change should be added to prevent an unnecessary second invocation of FindAndProcessHandler. This issue is the root cause of what is happening here.

  2. When you rethrow using throw inside of a catch block, if whatever was thrown and then caught is derived from System.Management.Automation.RuntimeException, it will be rethrown as is. Otherwise it will be converted into a System.Management.Automation.RuntimeException and then thrown. Conversely, if instead you rethrow by invoking throw $_ inside of the catch block, the ErrorRecord that was created to identify the error and that is stored in $_ will be thrown. From an end user perspective, this seems odd, since they are both inside of the same catch block and appear like they should do the same thing; however, I believe that detail is moot once the first issue is addressed.

Alternative workaround

An alternative workaround for the first issue than what was suggested above is to wrap the exception in an ErrorRecord, and then throw the ErrorRecord. You can see how the wrapping is done in the script that demonstrates that issue below.

Test script demonstrating first issue

To demonstrate the first issue, see the following script:

function Test-ExceptionHandling {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Type]
        $InnerExceptionType,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Type]
        $OuterExceptionType,

        [Parameter(Mandatory)]
        [ValidateSet('Exception','ErrorRecord','RuntimeException')]
        [System.String]
        $ThrowType
    )
    function Write-ErrorInfo {
        param($ErrorRecord)
        "Caught: $($ErrorRecord.Exception.foreach('GetType').FullName)"
        "Category: $($ErrorRecord.CategoryInfo.Category)"
        "Message: $($ErrorRecord.Exception.Message)"
        "InnerException: $($ErrorRecord.Exception.InnerException.foreach('GetType').FullName)"
    }

    Invoke-Expression -Command @"
try {
    `$ine = `$InnerExceptionType::new("My inner exception")
    `$oute =  `$OuterExceptionType::new("My outer exception", `$ine)
    `$er = [System.Management.Automation.ErrorRecord]::new(`$oute, `$oute.Message, 'OperationStopped', `$null)
    `$rte = [System.Management.Automation.RuntimeException]::new(`$oute.Message, `$oute, `$er)
    switch (`$ThrowType) {
        'Exception' {
            throw `$oute
        }
        'ErrorRecord' {
            throw `$er
        }
        'RuntimeException' {
            throw `$rte
        }
    }
} catch [$($InnerExceptionType.FullName)] {
    Write-ErrorInfo `$_
} catch [$($OuterExceptionType.FullName)] {
    Write-ErrorInfo `$_
} catch {
    'Caught: Catch all'
}
"@
}

# This works
Test-ExceptionHandling -InnerExceptionType System.ArgumentException -OuterExceptionType System.Management.Automation.ItemNotFoundException -ThrowType RuntimeException

# As does this
Test-ExceptionHandling -InnerExceptionType System.ArgumentException -OuterExceptionType System.Management.Automation.ItemNotFoundException -ThrowType ErrorRecord

# But when throwing the unwrapped raw exception, the bug is exposed, with the error handler catching the inner exception (something that should not be possible)
Test-ExceptionHandling -InnerExceptionType System.ArgumentException -OuterExceptionType System.Management.Automation.ItemNotFoundException -ThrowType Exception

Now that the issue has been identified, would you like it fixed @SteveL-MSFT?

All 9 comments

@TravisEz13 @SteveL-MSFT @BrucePay Is there any particular reason this behaviour is the way it is, and should we seek to design out a more consistent and reliable system for handling errors?

As it currently stands, it's not only difficult to _write_ scripts with this caveat (you have to use wordier and more complex alternatives like $PSCmdlet.ThrowTerminatingError(), or the workarounds @Jaykul describes), but as an end-user it also means that behaviour when applying different preference variables or parameters can't be relied upon for script modules. Practically ever module becomes a case-by-case "did they jump through all X hoops to make sure their error handling works".

In my opinion, we should be attempting to simplify working with errors, not introducing additional complications.

Recently I found myself digging into this issue while troubleshooting something related that I was working on in the debugger and compiler, and there are actually a few specific issues identified above that need to be discussed separately, as follows:

Issue descriptions

  1. Exceptions derived from System.Management.Automation.RuntimeException that contain inner exceptions cannot be caught by the outer exception type if the thing that was thrown was the raw exception (as opposed to an ErrorRecord or a wrapping RuntimeException). They can, however, be caught using the inner exception type (which should never be possible, if PowerShell aligns itself to similar designs in other languages like C#). This is an issue in lines 1464-1466 of the FindMatchingHandler static method, which should create a new ErrorRecord that wraps the RuntimeException and assign those two variables differently if the parent (the exception in the rte variable itself) contains the exception, as can be seen by inspecting the ErrorRecord property on the RuntimeException. Additionally a minor logic change should be added to prevent an unnecessary second invocation of FindAndProcessHandler. This issue is the root cause of what is happening here.

  2. When you rethrow using throw inside of a catch block, if whatever was thrown and then caught is derived from System.Management.Automation.RuntimeException, it will be rethrown as is. Otherwise it will be converted into a System.Management.Automation.RuntimeException and then thrown. Conversely, if instead you rethrow by invoking throw $_ inside of the catch block, the ErrorRecord that was created to identify the error and that is stored in $_ will be thrown. From an end user perspective, this seems odd, since they are both inside of the same catch block and appear like they should do the same thing; however, I believe that detail is moot once the first issue is addressed.

Alternative workaround

An alternative workaround for the first issue than what was suggested above is to wrap the exception in an ErrorRecord, and then throw the ErrorRecord. You can see how the wrapping is done in the script that demonstrates that issue below.

Test script demonstrating first issue

To demonstrate the first issue, see the following script:

function Test-ExceptionHandling {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Type]
        $InnerExceptionType,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Type]
        $OuterExceptionType,

        [Parameter(Mandatory)]
        [ValidateSet('Exception','ErrorRecord','RuntimeException')]
        [System.String]
        $ThrowType
    )
    function Write-ErrorInfo {
        param($ErrorRecord)
        "Caught: $($ErrorRecord.Exception.foreach('GetType').FullName)"
        "Category: $($ErrorRecord.CategoryInfo.Category)"
        "Message: $($ErrorRecord.Exception.Message)"
        "InnerException: $($ErrorRecord.Exception.InnerException.foreach('GetType').FullName)"
    }

    Invoke-Expression -Command @"
try {
    `$ine = `$InnerExceptionType::new("My inner exception")
    `$oute =  `$OuterExceptionType::new("My outer exception", `$ine)
    `$er = [System.Management.Automation.ErrorRecord]::new(`$oute, `$oute.Message, 'OperationStopped', `$null)
    `$rte = [System.Management.Automation.RuntimeException]::new(`$oute.Message, `$oute, `$er)
    switch (`$ThrowType) {
        'Exception' {
            throw `$oute
        }
        'ErrorRecord' {
            throw `$er
        }
        'RuntimeException' {
            throw `$rte
        }
    }
} catch [$($InnerExceptionType.FullName)] {
    Write-ErrorInfo `$_
} catch [$($OuterExceptionType.FullName)] {
    Write-ErrorInfo `$_
} catch {
    'Caught: Catch all'
}
"@
}

# This works
Test-ExceptionHandling -InnerExceptionType System.ArgumentException -OuterExceptionType System.Management.Automation.ItemNotFoundException -ThrowType RuntimeException

# As does this
Test-ExceptionHandling -InnerExceptionType System.ArgumentException -OuterExceptionType System.Management.Automation.ItemNotFoundException -ThrowType ErrorRecord

# But when throwing the unwrapped raw exception, the bug is exposed, with the error handler catching the inner exception (something that should not be possible)
Test-ExceptionHandling -InnerExceptionType System.ArgumentException -OuterExceptionType System.Management.Automation.ItemNotFoundException -ThrowType Exception

Now that the issue has been identified, would you like it fixed @SteveL-MSFT?

That's some supreme sleuthing you've done there, Kirk, nice work! 馃槉

Just to add one more thought related to the second "issue" identified above so that the committee can consider it.

From within a catch block, invoking throw and throw $_ are effectively doing the same thing. The end result is two ErrorRecord objects that are not the same (they have a different hash code because they are generated independently by PowerShell), but the properties on those ErrorRecord objects are identical. Given that is the case, wouldn't it make sense for the PowerShell compiler to treat throw and throw $_ both as rethrows, in this method, so that they both compile and function the same way?

On second thought, I don't think that's a good idea because you could store $_ in another variable and then throw that variable later, and it wouldn't be handled quite the same way. The best thing to do for throw $_ if the first issue is addressed would probably be a PowerShell Script Analyzer rule that generates a warning recommending users simply use throw instead.

@PowerShell/powershell-committee reviewed this, we believe the fundamental issue is that there are many cases in PowerShell where exceptions get wrapped. This makes error handling difficult. We agreed that we should allow the user to catch the inner exception. Proposal is that in our catch code, we should recurse into it looking for a match and return that exception to the catch if found. We should not make changes to throw behavior. @JamesWTruher offered to author a RFC for this

@SteveL-MSFT Just to make sure I understand, when you say "we should allow the user to catch the inner exception", you're only suggesting that PowerShell try/catch blocks be able to catch inner exceptions that are automatically wrapped by PowerShell with other exceptions such as RuntimeException, and not inner exceptions in general, correct?

@KirkMunro we didn't differentiate between PS wrapped or non-PS wrapped exceptions. The idea is that PS would look through all the inner exceptions for a match regardless of who put it there. This is a technical breaking change, but in our discussion we agreed that since this is an error handling case, those scripts were already broken if they weren't correctly catching the exception.

@SteveL-MSFT and @JamesWTruher: This warrants some more discussion in the committee or here on this issue discussion because you made the wrong decision.

Inner exceptions should not be catchable because they've already been handled. C# works this way. Java as well. I can do more research, but I suspect other programming languages behave the same way. When you have a method/command/script that invokes something inside of a try/catch block and internally catches that exception, the code is handling the exception. If it wants to surface the exception as-is, it just rethrows it. Otherwise, it may suppress it, or it may generate a new exception based on that exception and assign the original exception as the inner exception. This is known as exception chaining.

If I write some code that may throw exceptions, it is advantageous to the caller to know what type of exceptions may be thrown. For example, see any number of methods in .NET such as this one which identifies the types of exceptions that it may throw. Unless the logic within those methods specifically rethrows an exception of a certain type, you will never, ever see that documentation indicate types of inner exceptions as exceptions that it throws _because they are already handled_.

PowerShell should learn from these other languages and be no different when it comes to exception chaining. Users should not be allowed to catch inner exceptions. The exception to that rule (pun intended) when it comes to PowerShell is with wrapper exceptions. PowerShell uses certain exception types to marshal exceptions through the engine and surface them to the user. In those cases, the user needs to be able to catch the original exception, not the wrapper/helper that PowerShell puts on top of it.

If you look in Issue 1 of my earlier post, it describes one place where PowerShell can be updated to properly propagate exceptions through the system so that they can be properly caught (the actual exception, not the wrapper). Those are the types of changes that need to be made through PRs -- identify where PowerShell wraps exceptions, and make sure that those exceptions are wrapped properly so that the correct exception can be caught by the end user.

I have obviously explained this very poorly if you think that making me able to catch inner exceptions would help. The original problem I was trying to work around by using throw $_ instead of throw is the fact that I am already catching inner exceptions

I'm trying NOT to catch inner exceptions!

I mean, I literally wrote: We should be able to catch the _outer_ exception ...

See #9689

Was this page helpful?
0 / 5 - 0 ratings