PowerShell try/catch Is Not Catching Your Error

Your try/catch failed and the catch didn’t run at all; the error outputted in red. Your script continued as though everything was fine, and your catch was completely ignored.

There’s nothing wrong. PowerShell has two types of error, and try/catch only ever works for one type of error.

All that came after was done in PowerShell 7.5.8

The demonstration

try {
    Get-Item 'C:\does\not\exist\nothing.txt'
    "...execution continued past the failing line"
} catch {
    "CAUGHT: $($_.Exception.Message)"
}
Get-Item:
Line |
   6 |      Get-Item $missing
     |      ~~~~~~~~~~~~~~~~~
     | Cannot find path 'C:\does\not\exist\nothing.txt' because it does not exist.
...execution continued past the failing line

The error was written out. The catch block never ran, and the line after it did.

Now the same thing with four extra words:

try {
    Get-Item 'C:\does\not\exist\nothing.txt' -ErrorAction Stop
} catch {
    "CAUGHT: $($_.Exception.Message)"
}
CAUGHT: Cannot find path 'C:\does\not\exist\nothing.txt' because it does not exist.

Why

Most cmdlet errors are non-terminating. The cmdlet tells you there’s an issue and powershell keeps on trucking since typically if you’re sending 1000 objects through a cmdlet, you probably want the remaining 999 to get through too.

The try/catch only responds to terminating errors.

-ErrorAction Stop makes a non-terminating error terminating in just that instance. It is all there is to it and that is why most examples of PowerShell code out there contain try/catch statements that do absolutely nothing.

.

Write-Error is not throw

Worth knowing if you are writing your own functions:

try { Write-Error 'this is a Write-Error' } catch { "caught" }
try { throw 'this is a throw' }             catch { "caught: $($_.Exception.Message)" }
Write-Error: this is a Write-Error
caught: this is a throw

Write-Error will generate a non-terminating error, which cannot be caught. throw is a terminating action and can be caught.

Therefore, if a function uses Write-Error to indicate error, no one calling that function can catch the error unless -ErrorAction Stop is also specified. Use throw when you mean stop.

Turning it on for a whole script

Rather than putting -ErrorAction Stop on every line:

$ErrorActionPreference = 'Stop'
CAUGHT without -ErrorAction on the cmdlet

Put this at the beginning of a script and every cmdlet failure is now terminating. In most cases where an admin script is modifying something, this is precisely what you should be doing. It’s better to stop at the first issue than to continue and complete only part of the task.

It is scoped. Set this in a function and it only affects the function and not the calling code. This is nice and means that putting this in your profile won’t help your scripts.

.

Catching one kind of error and not others

A bare catch catches everything, including the failures you had not thought about. You can be specific:

try {
    Get-Item $path -ErrorAction Stop
} catch [System.Management.Automation.ItemNotFoundException] {
    "matched the typed catch: file not found"
} catch {
    "fell through to the generic catch"
}
matched the typed catch: file not found

To find the type name for something you are actually hitting, let it fail once and ask:

$Error[0].Exception.GetType().FullName
$Error[0].FullyQualifiedErrorId
$Error[0].CategoryInfo.Category
System.Management.Automation.ItemNotFoundException
PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand
ObjectNotFound

FullyQualifiedErrorId is the precise one. It names both the problem and the cmdlet that raised it, which is what you want when two different cmdlets can produce the same exception type.

Collecting errors without stopping

Sometimes you want the loop to finish and a list of what went wrong:

Get-Item $path -ErrorAction SilentlyContinue -ErrorVariable itemErr

$itemErr.Count
$itemErr[0].Exception.Message
1
Cannot find path 'C:\does\not\exist\nothing.txt' because it does not exist.

Use +itemErr with a plus sign to append across a loop rather than overwrite each time.

The one that catches everybody: native commands

try/catch does not work on robocopy, git, msiexec or anything else that is not a cmdlet.

try {
    cmd.exe /c "exit 7"
    "no exception was raised"
} catch {
    "caught"
}
no exception was raised

External programs do not raise PowerShell exceptions. They set an exit code, and you have to check it yourself:

cmd.exe /c "exit 7"
$?              # False
$LASTEXITCODE   # 7

When tested right after the call, because $? is the most recent thing to happen. Just insert the Write-Host statement between your command and your test, and now you are testing the output of the Write-Host statement. That is what I did when testing for this post.

So anything external

robocopy $source $dest /E
if ($LASTEXITCODE -ge 8) { throw "robocopy failed with $LASTEXITCODE" }

Note the -ge 8. Robocopy uses 0-7 for various kinds of success, so testing -ne 0 would fail every time it copied something. Check what your tool’s exit codes actually mean rather than assuming zero is the only good one.

PowerShell 7 can do this for you

There is a setting for it, and it is off by default:

$PSNativeCommandUseErrorActionPreference   # False

Turn it on, with $ErrorActionPreference set to Stop:

$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'

try { cmd.exe /c "exit 7" } catch { "caught = true" }
caught = true

A non-zero exit code now raises a terminating error you can catch like anything else. Worth knowing, and worth being careful with, because anything that uses non-zero exit codes for non-failures, robocopy included, will start throwing at you.

finally

try {
    Get-Item $path -ErrorAction Stop
} catch {
    "catch ran"
} finally {
    "finally ran"
}
catch ran
finally ran

Runs whether or not anything failed. Use it for the cleanup that has to happen either way, like disconnecting a session or removing a temporary file.

What I actually put in a script

$ErrorActionPreference = 'Stop'

try {
    # everything that changes something
}
catch {
    Write-Error "Failed at $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-PnPOnline -ErrorAction SilentlyContinue
}

$.InvocationInfo.ScriptLineNumber gives you the location, something that is not indicated by the error message alone. The throw statement just before the closing curly brace will simply rethrow the original error without suppressing it.

Handling the error and moving on without saying anything is far worse than not handling the error at all. At least the red text was truthful.

Related posts

Similar Posts