1
votes

Try-catch appears to not reliably trap all errors. Try-catch on get-ChildItem does not report all access errors that are reported outside of try-catch.

Edit: It is not true that try-catch is being unreliable, and there is a sound reason why it reported only one error. See my comment to the accepted answer to see my misunderstanding behind this question.

In Windows 10 Pro 64, running PowerShell 5.1, this script:

$sScriptName  = "ErrorTest.ps1"

write-host ("I will get the file structure of ""C:\Windows\System32"", but this yields two access-denied errors:")

$aoFileSystem = @(get-ChildItem "C:\Windows\System32" -recurse -force)

write-host ("I will now do it again and trap for those errors. But I only get one of them:")

try {$aoFileSystem = @(get-ChildItem $sPath -recurse -force -ErrorAction Stop)}
catch [System.UnauthorizedAccessException]
   {$sErrorMessage = $_.ToString()
    write-host ("System.UnauthorizedAccessException: " + $sErrorMessage.substring(20, $sErrorMessage.length - 32))}

running in the ISE as administrator, gets this output:

PS C:\WINDOWS\system32> D:\<path>\ErrorTest.ps1
I will get the file structure of "C:\Windows\System32", but this yields two access-denied errors:
get-ChildItem : Access to the path 'C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5' is denied.
At D:\<path>\ErrorTest.ps1:5 char:19
+ ... aoFileSystem = @(get-ChildItem "C:\Windows\System32" -recurse -force)
+                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : PermissionDenied: (C:\Windows\Syst...che\Content.IE5:String) [Get-ChildItem], UnauthorizedAccessException
    + FullyQualifiedErrorId : DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand

get-ChildItem : Access to the path 'C:\Windows\System32\LogFiles\WMI\RtBackup' is denied.
At D:\<path>\ErrorTest.ps1:5 char:19
+ ... aoFileSystem = @(get-ChildItem "C:\Windows\System32" -recurse -force)
+                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : PermissionDenied: (C:\Windows\Syst...es\WMI\RtBackup:String) [Get-ChildItem], UnauthorizedAccessException
    + FullyQualifiedErrorId : DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand

I will now do it again and trap for those errors. But I only get one of them:
System.UnauthorizedAccessException: C:\WINDOWS\system32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5

PS C:\WINDOWS\system32> 

I want to log the access errors, but when I try to trap for them, I only get the first one. What do I need to do to get the second one to show up when I trap for them?

Edit: I got a message telling me that I need to edit my question to explain how it's different from Can PowerShell trap errors in GetChildItem and continue looping?. So I'll repeat here what I said in response to the comment below from Scepticalist. That question is for get-ChildItem in a ForEach loop. My problem does not involve such a loop. Still, I tried something from the accepted answer there, using -ErrorAction SilentlyContinue, but that hides the errors without trapping them. However, in the solution I found and am about to post as an answer, I do use -ErrorAction SilentlyContinue in combination with -ErrorVariable.

3
This has been answered here: stackoverflow.com/questions/6942747/… - Scepticalist
@Scepticalist : No, I looked at that. That's for get-ChildItem in a ForEach loop. My problem does not involve such a loop. Still, I tried the accepted answer there, which is to use -ErrorAction SilentlyContinue, and that only hides the errors, does not trap them. - NewSites
then i suspect the only way to do what you need is to build a new array as you go, marking the denied files. - Scepticalist
Why even use try-catch then? Just use the $error variable to capture the same data. - AdminOfThings

3 Answers

1
votes
  • The errors emitted by Get-ChildItem "C:\Windows\System32" -recurse -force are non-terminating errors, and, given that such errors by definition don't terminate a command, a single command may emit multiple errors of this type.

    • All non-terminating errors emitted by a cmdlet can be captured in a designated variable whose name you pass to the -ErrorVariable common parameter.
    • Additionally, all errors (both non-terminating and terminating ones) are by default recorded in the session-global automatic $Error collection variable.
  • try / catch only acts on terminating errors, so by default it has no effect on commands that emit non-terminating errors only, such as Get-ChildItem "C:\Windows\System32" -recurse -force

    • You can instruct the command to turn the first non-terminating error encountered into a terminating one by adding -ErrorAction Stop (using the -ErrorAction common parameter), in which case an enclosing try / catch does catch the error, but note that execution then stops after the first error encountered either way.

Caveat: There are two types of terminating errors (and the fact that there are may be a historical accident); while both types can be caught with try / catch, their default behavior differs:

  • Statement-terminating errors, which only terminate the statement at hand and, after emitting an error message, by default continue script execution; typically, these errors are emitted by (compiled) cmdlets if they encounter errors that aren't limited to the input at hand and doesn't allow them to continue processing further input.

  • Script-terminating errors (runspace-terminating errors), which abort processing altogether by default. PowerShell code that uses the Throw statement generates such errors, as does passing -ErrorAction Stop to a command that emits non-terminating errors.

    • Caveat: While -ErrorAction Stop has no effect on statement-terminating errors, the seemingly equivalent preference-variable setting, $ErrorActionPreference = 'Stop', unexpectedly promotes statement-terminating errors to script-terminating ones.

Further reading:

  • For a comprehensive overview of PowerShell's - bewilderingly complex - error handling, see this GitHub docs issue.

  • As for when to report a terminating vs. a non-terminating error when authoring commands, see this answer.

0
votes

I did some research on the ErrorAction parameter, and doing that, discovered the ErrorVariable parameter. (Both are common parameters, so they don't show up in the documentation of get-ChildItem.) And from there, I was able to figure out how to correctly trap for the errors.

The new script below shows four ways of doing this. Methods 2 and 3 work okay. Method 4 doesn't work because it incorrectly attempts to use the parameter InputObject in ForEach-object. Method 1, the original try-catch method, doesn't work and I still don't know why. This is troubling because it's important that error trapping works as expected. If I did not know to expect two errors, I would not have known that try-catch was not giving me the right output.

I am not accepting this (my own) answer because, although the script below shows two methods to correctly trap for these errors, it would still be better if someone can explain why try-catch does not work for this.

New script:

$sScriptName  = "ErrorTest.ps1"

$sPath = "C:\Windows\System32"

write-host ("I will get the file structure of """ + $sPath + """, but this yields two access-denied errors:")
$aoFileSystem = @(get-ChildItem $sPath -recurse -force)

write-host ("I will now do it again and trap for those errors.")

write-host ("`r`nMethod 1: Original method using try-catch (incorrect results; only finds one of the two errors; why?):")
try {$aoFileSystem = @(get-ChildItem $sPath -recurse -force -ErrorAction Stop)}
catch [System.UnauthorizedAccessException]
   {$sErrorMessage = $_.ToString()
    write-host ("System.UnauthorizedAccessException: " + $sErrorMessage.substring(20, $sErrorMessage.length - 32))}

write-host ("`r`nGet array for Methods 2 to 4.")
$aoFileSystem = @(get-ChildItem $sPath -recurse -force -ErrorAction SilentlyContinue -ErrorVariable aoChildItemError)

write-host ("`r`nMethod 2: Output by piping to ForEach-object (correct results):")
$aoChildItemError | 
    ForEach-object `
       {$oErrorRecord = $_
        write-host ($oErrorRecord.CategoryInfo.reason + ": """ + $oErrorRecord.TargetObject + """")}

write-host ("`r`nMethod 3: Output by for loop (correct results):")
for ($nCount = 0; $nCount -lt $aoChildItemError.count; $nCount++)
   {$oErrorRecord = $aoChildItemError[$nCount]
    write-host ($oErrorRecord.CategoryInfo.reason + ": """ + $oErrorRecord.TargetObject + """")}

write-host ("`r`nMethod 4: Output by ForEach-object loop without pipeline (incorrect results because it incorrectly attempts to use the parameter ""InputObject"" in ""ForEach-object""):")
ForEach-object -InputObject $aoChildItemError `
   {$oErrorRecord = $_
    write-host ($oErrorRecord.CategoryInfo.reason + ": """ + $oErrorRecord.TargetObject + """")}

New output:

PS C:\WINDOWS\system32> D:\_\z-temp\ErrorTest.ps1
I will get the file structure of "C:\Windows\System32", but this yields two access-denied errors:
get-ChildItem : Access to the path 'C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5' is denied.
At D:\_\z-temp\ErrorTest.ps1:6 char:19
+ $aoFileSystem = @(get-ChildItem $sPath -recurse -force)
+                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : PermissionDenied: (C:\Windows\Syst...che\Content.IE5:String) [Get-ChildItem], UnauthorizedAccessException
    + FullyQualifiedErrorId : DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand

get-ChildItem : Access to the path 'C:\Windows\System32\LogFiles\WMI\RtBackup' is denied.
At D:\_\z-temp\ErrorTest.ps1:6 char:19
+ $aoFileSystem = @(get-ChildItem $sPath -recurse -force)
+                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : PermissionDenied: (C:\Windows\Syst...es\WMI\RtBackup:String) [Get-ChildItem], UnauthorizedAccessException
    + FullyQualifiedErrorId : DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand

I will now do it again and trap for those errors.

Method 1: Original method using try-catch (incorrect results; only finds one of the two errors; why?):
System.UnauthorizedAccessException: C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5

Get array for Methods 2 to 4.

Method 2: Output by piping to ForEach-object (correct results):
UnauthorizedAccessException: "C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5"
UnauthorizedAccessException: "C:\Windows\System32\LogFiles\WMI\RtBackup"

Method 3: Output by for loop (correct results):
UnauthorizedAccessException: "C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5"
UnauthorizedAccessException: "C:\Windows\System32\LogFiles\WMI\RtBackup"

Method 4: Output by ForEach-object loop without pipeline (incorrect results because it incorrectly attempts to use the parameter "InputObject" in "ForEach-object"):

UnauthorizedAccessException UnauthorizedAccessException : " C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\Content.IE5 C:\Window
s\System32\LogFiles\WMI\RtBackup "

PS C:\WINDOWS\system32>  
-1
votes

Quite simple! You specified the parameter -ErrorAction Stop in your second directory access statement. Switch it to -ErrorAction Continue and you get both error messages.