1

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.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
NewSites
  • 1,402
  • 2
  • 11
  • 26
  • This has been answered here: https://stackoverflow.com/questions/6942747/can-powershell-trap-errors-in-getchilditem-and-continue-looping – Scepticalist Oct 25 '19 at 13:50
  • Possible duplicate of [Can PowerShell trap errors in GetChildItem and continue looping?](https://stackoverflow.com/questions/6942747/can-powershell-trap-errors-in-getchilditem-and-continue-looping) – Scepticalist Oct 25 '19 at 13:51
  • @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 Oct 25 '19 at 14:03
  • 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 Oct 25 '19 at 14:26
  • Why even use try-catch then? Just use the `$error` variable to capture the same data. – AdminOfThings Oct 25 '19 at 14:34

3 Answers3

1
  • 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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 2
    Thank you for explaining very clearly my dumb mistake. I see now that I did not read the `ErrorAction` documentation (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters) carefully enough. It says, "`-ErrorAction:Stop` displays the error message and stops executing the command." So of course it will only find one error because the statement stops executing before it gets to the second one! So I'm relieved to know that try-catch is behaving correctly, and it was my mistake to think otherwise. – NewSites Nov 04 '19 at 02:35
0

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>  
NewSites
  • 1,402
  • 2
  • 11
  • 26
  • 1
    The `foreach-object` fails because you are processing the entire object array as one object. You want to use the pipeline to process each element of the array. Use `$aoChildItemError | Foreach-Object { ..... }` – AdminOfThings Oct 25 '19 at 17:12
  • @AdminOfThings : I've taken your suggestion of doing it by pipeline and added that way to the script. But this doesn't explain why `ForEach` without the pipeline doesn't work. `ForEach` is supposed to do its process block on each element of an input array separately, but it's somehow merging the elements of the array here. I'd like to understand why and fix it. – NewSites Oct 26 '19 at 12:55
  • In previous comment, `ForEach` = `ForEach-object`. – NewSites Oct 26 '19 at 13:06
  • Yes, it does explain it. If you have an array called $array, it is a single object that it will contain multiple objects. If you pass $array into -InoutObject, it will treat $array as a single object and not break it down into the multiple objects it contains. When you push an array down the pipeline, it will pass each individual array item one at a time. – AdminOfThings Oct 26 '19 at 13:26
  • Okay, I see you're right. The documentation (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/ForEach-Object?view=powershell-5.1) says "When you use the `InputObject` parameter with `ForEach-Object`, instead of piping command results to `ForEach-Object`, the `InputObject` value is treated as a single object." But this seems weird. What is the point of a "ForEach" if there is no "each" to do "for" on? Is there really no way to get `ForEach-Object` to act on the individual elements of an array? – NewSites Oct 26 '19 at 13:42
  • 1
    `$array | foreach-object` will act on individual elements. – AdminOfThings Oct 26 '19 at 13:45
  • Excuse me. Rephrase: Is there really no way to get `ForEach-Object` with `InputObject` to act on the individual elements of an array? Of not, it seems that `ForEach-Object` with `InputObject` is completely useless. – NewSites Oct 26 '19 at 13:48
  • 1
    I've posted that questions now separately: https://stackoverflow.com/questions/58589314/powershell-what-is-the-point-of-foreach-object-with-inputobject . – NewSites Oct 28 '19 at 10:40
  • While the answer by @mklement0 explained what was going on with try-catch, that answer means that even though try-catch was behaving properly, it cannot be used for what I needed. This answer, in methods 2 and 3, shows what needs to be done to get what I needed. – NewSites Nov 04 '19 at 02:56
-1

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.

Ali
  • 55
  • 1
  • 4
  • 1
    Are you sure? If I try that, i get 2 uncatched Errors. As far as I understood, the topic starter wants to trap those errors ... – Peter Pesch Oct 25 '19 at 12:52
  • @PeterPesch 's comment agrees with what I got. I was excited to try this simple solution, but when I changed "Stop" to "Continue" in the script, the result was that the try-catch gave me the same output as I got from `get-ChildItem` without the try-catch, i.e., both errors untrapped. So this is not the right solution. – NewSites Oct 25 '19 at 13:00