Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Fix try/catch to choose the more specific exception handler#2429

Merged
lzybkr merged 8 commits into
PowerShell:masterPowerShell/PowerShell:masterfrom
daxian-dbw:catchdaxian-dbw/PowerShell:catchCopy head branch name to clipboard
Dec 9, 2016
Merged

Fix try/catch to choose the more specific exception handler#2429
lzybkr merged 8 commits into
PowerShell:masterPowerShell/PowerShell:masterfrom
daxian-dbw:catchdaxian-dbw/PowerShell:catchCopy head branch name to clipboard

Conversation

@daxian-dbw

@daxian-dbw daxian-dbw commented Oct 5, 2016

Copy link
Copy Markdown
Member

Fix #2293

Idea behind the Fix
After selecting a handler, for both try/catch and trap{}, we set $_ with new ErrorRecord(rte.ErrorRecord, exceptionToPass). The exceptionToPass will be adopted only if (rte.ErrorRecord.Exception is ParentContainsErrorRecordException) == true. So basically $_.Exception will always be rte.ErrorRecord.Exception unless it's a ParentContainsErrorRecordException instance. So we should just select the handler based on rte.ErrorRecord.Exception if it's not ParentContainsErrorRecordException. When it is ParentContainsErrorRecordException, then we go through the original code path to determine what exception to pass to user.

The old fix would cause unacceptable breaking changes (see the discussion below). The following is the new fix:

First rank the given exception types -- a smaller rank indicates a more specific exception type, for example, rank == 0 means the corresponding exception type is not derived by another other given exception types, while rank == 1 means the corresponding exception type is derived by another exception type in the given types. CatchAll is considered to be derived by any other exception type.

Then during the search, we try all existing searching methods in the same existing order, as long as we haven't found a handler whose exception type's rank is 0.
When a new handler is found, we use the following algorithm to decide whether to replace the current handler (the handler that was found before the new handler):

  • If new-rank is less than current-rank -- meaning the new handler is more specific, then we update the current result with it.
  • If new-rank is more than current-rank -- meaning the new handler is less specific, then we do NOT change the current result.
  • If new-rank is equal to current-rank, we do NOT change the current result UNLESS the current handler is catch-all. (This is to keep the original behavior -- prefer to use the later found exception as the exception-to-pass-in if all exceptions result in the catch-all handler.

With the Fix

PS D:\> try {
>>     Get-ChildItem c:\xyzzy -ea Stop
>> } catch [System.Management.Automation.ItemNotFoundException] {
>>     "ItemNotFoundException caught"
>> } catch [System.Exception] {
>>     "System.Exception caught"
>> }
ItemNotFoundException caught
PS D:\>
PS D:\> try {
>>     Get-ChildItem c:\xyzzy -ea Stop
>> } catch [System.Management.Automation.ItemNotFoundException] {
>>     "ItemNotFoundException caught"
>> } catch [System.Management.Automation.RuntimeException] {
>>     "RuntimeException caught"
>> }
ItemNotFoundException caught

Task

  • Refine the fix
  • Run our legacy tests to find potential regressions (ran the Github nightly build tests)
  • Add tests

@msftclas

msftclas commented Oct 5, 2016

Copy link
Copy Markdown

Hi @daxian-dbw, I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution!


It looks like you're a Microsoft contributor (Dongbo Wang). If you're full-time, we DON'T require a Contribution License Agreement. If you are a vendor, please DO sign the electronic Contribution License Agreement. It will take 2 minutes and there's no faxing! https://cla.microsoft.com.

TTYL, MSBOT;

@HemantMahawar HemantMahawar added the WG-Language parser, language semantics label Oct 6, 2016
@lzybkr

lzybkr commented Oct 6, 2016

Copy link
Copy Markdown
Contributor

The potential breaking changes comes from trying to catch ParentContainsErrorRecordException?

If that's it, it seems safe. Most people just catch everything anyway.

I'd like some investigation to know for sure that ActionPreferenceStopException and CmdletInvocationException are always wrapped in a ParentContainsErrorRecordException.

@daxian-dbw

Copy link
Copy Markdown
Member Author

I will investigate ActionPreferenceStopException and CmdletInvocationException further.

@daxian-dbw

Copy link
Copy Markdown
Member Author

@lzybkr it turns out it causes quite a few regressions ☹️ for example:

Before the change

PS:20> try { dir c:\xyzz -ea Stop }
>>     catch [System.Management.Automation.ActionPreferenceStopException] {
>>        "ActionPreferenceStopException Caught" }
ActionPreferenceStopException Caught

PS:22> try { Invoke-Expression "gcm -Name" }
>>     catch [System.Management.Automation.CmdletInvocationException] {
>>        "CmdletInvocationException Caught" }
CmdletInvocationException Caught

After the change

PS C:\> try { dir c:\xyzz -ea Stop }
>>      catch [System.Management.Automation.ActionPreferenceStopException] {
>>         "ActionPreferenceStopException Caught" }
dir : Cannot find path 'C:\xyzz' because it does not exist.
At line:1 char:7
+ try { dir c:\xyzz -ea Stop }
+       ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\xyzz:String) [Get-ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

PS C:\> try { Invoke-Expression "gcm -Name" }
>>      catch [System.Management.Automation.CmdletInvocationException] {
>>         "CmdletInvocationException Caught" }
Invoke-Expression : Missing an argument for parameter 'Name'. Specify a parameter of type 'System.String[]' and try again.
At line:1 char:7
+ try { Invoke-Expression "gcm -Name" }
+       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Invoke-Expression], ParameterBindingException
    + FullyQualifiedErrorId : MissingArgument,Microsoft.PowerShell.Commands.InvokeExpressionCommand

And the awkward part of the current behavior is that sometimes $_.Exception in the catch block is not the same kind of exception that the catch block is supposed to capture, take the same commands as examples:

PS:24> try { dir c:\xyzz -ea Stop }
>>     catch [System.Management.Automation.ActionPreferenceStopException] {
>>        $_.Exception.GetType().FullName }
System.Management.Automation.ItemNotFoundException

PS:25> try { Invoke-Expression "gcm -Name" }
>>     catch [System.Management.Automation.CmdletInvocationException] {
>>        $_.Exception.GetType().FullName }
System.Management.Automation.ParameterBindingException

You can see that $_.Exception caught by catch [ActionPreferenceStopException] and catch [CmdletInvocationException] are ItemNotFoundException and ParameterBindingException respectively.

@daxian-dbw

daxian-dbw commented Nov 18, 2016

Copy link
Copy Markdown
Member Author

The root problem is we don't have a specific exception to search for handler. Instead, we have multiple candidate exceptions from which we pick up to suit the given handlers.

The fix needs to be re-worked, and I think we need to change new ErrorRecord(ErrorRecord, Exception) to allow forceful replacement of the exception. it turns out we cannot just change ErrorRecord.Exception forcefully to be the one we pick for a handler, because people are depending on $_.Exception to get the real exception. Example: https://github.com/eccentricDBA/PowerShell/blob/02734d1b415deb74b46d38fae77b8d98cce5ede2/Demos/5_Test-Connection.ps1#L16.

Therefore, new ErrorRecord(ErrorRecord, Exception) should continue to behave the current way -- only replace the exception if ErrorRecord.Exception is ParentContainsErrorRecordException.

@daxian-dbw daxian-dbw force-pushed the catch branch 2 times, most recently from c29372e to a7e0e7a Compare November 21, 2016 07:32
@PowerShellTeam PowerShellTeam added the Review - Needed The PR is being reviewed label Nov 22, 2016
}

It "Catch by type #4" {
$a = . { try { 1; $a = 0; 1/$a; "test failed" } catch [DivideByZeroException],[ArgumentNullException] { 2 } }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$a = . { try { 1; $a = 0; 1/$a; "test failed" } catch [DivideByZeroException],[ArgumentNullException] { 2 } } [](start = 12, length = 109)

should this be explicit?
$a = . { try { 1; throw ([ArgumentNullException]::new("bad")) } catch [DivideByZeroException],[ArgumentNullException] { 2 } }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case was moved from source depot. It originally was catch [DivideByZeroException],[StackOverflowException], and it looks to me it tries to verify the DivideByZeroException thrown by 1/0. StackOverflowException is not in CoreCLR so I replaced it with ArgumentNullException.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yah - since the test immediately prior is catching DivideByZeroException I wasn't sure what this test was really doing. Should we have two tests? One to catch DivideByZeroException and on to catch ArgumentNullException (e.g., we can catch both exception types) - if we can improve on our existing tests and make them better/clearer that's awesome

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will add a test for ArgumentNullException.

}

# Disabled - Compiled script has differing (but better) behavior
It "return in try" -Skip:$true {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be marked as pending rather than skip?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will change it to -Pending.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@daxian-dbw

Copy link
Copy Markdown
Member Author

@lzybkr I think this try/catch fix is ready. Can you please review this PR?

finally
{
}
} | Should Not Throw

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should Not Throw doesn't do anything useful here - parse only tests will fail to parse, so nothing would run.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, if parsing failed, | Should Not Throw will never run. The original test was to make sure those use of try/catch would not cause parsing error.
How about I just have a dummy $true | Should Be $true following the try/catch blocks? So if no parsing error, the test will pass.

Context "Line after exception should not be invoked" {
It "line after exception should not be invoked" {
$a = . {trap {"trapped"; continue;}; . {"hello"; throw "exception"; "world"}}
$a.Length | Should Be 2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the test should be more explicit in the expected results so it's easier to see the expected flow and to avoid a cascade of failures from helping the test succeed when it should fail.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. I converted the Lite tests to Pester tests, but didn't look closely to their assert messages. Fixed.

foo 2
}

AssertArraysEqual $a (1, "finally: 1", "finally: 2", "return: 2")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've started using -join ' ' on the array so that I can use Should Be ... which somehow feels more idiomatic to Pester even if it's a little weird. This is just a comment to discuss, I'm not asking for a change.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will use -join ',' on array comparison in future. I used it in Trap.Tests.ps1.

@lzybkr lzybkr merged commit bbf1766 into PowerShell:master Dec 9, 2016
@daxian-dbw daxian-dbw deleted the catch branch December 9, 2016 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review - Needed The PR is being reviewed WG-Language parser, language semantics

Projects

None yet

Development

Successfully merging this pull request may close these issues.

catch [System.Exception] statement executes *before* more specific exception

7 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.