$get-help *error*
$
So this shocked the hell outta me.
Well, actually I lied, it's not quite that bad - this query actually returns one result, but it's for a function called Write-Error, which tells you nothing about how to do exception handling in PowerShell. And once you learn the two primary keywords for doing it (trap, throw), you won't get any help on those either. Turns out they just aren't documented in the current release, but the Error Handling and Debugging chapter of Jones & Hicks book on PowerShell is available on the web that will help you get started. FWIW, I also found a couple more chapters available online from Sapien Press. Thanks, guys!
I skimmed the chapter on the web and was able to get started. I'm recording a bit of what I discovered along the way so that others (myself included) will benefit from my research.
Here was the problem I wanted to solve. This particular problem isn't really that - it's nothing close to a showstopper for me, but I wanted to take the opportunity to learn how to do exception handling in PowerShell so that I would be ready when I really needed it. Anyway, I have this script that I use a lot to restart a process, which I'll call “foo“ for the sake of this post. Here's the two-liner script:
function restartFoo {
get-process “foo” | stop-process
c:\bin\foo.exe
}
It turns out that if there *are* no processes called “foo” running, get-process won't just send an empty list (too bad), rather it throws an InvalidOperationException. Here's how you'd normally trap such an exception in PowerShell:
function restartFoo {
trap { # this installs an error handler for the function
continue; # exiting a trap with "continue" says
# to continue on the next line of the script
}
get-process foo | stop-process
c:\bin\foo.exe
}
This was pretty much all I needed. It says that if there was any problem with the get-process line of the script, it should just continue on. So if there are no copies of "foo" running (or if stop-process fails for any reason) just skip that part. Pretty dumb error handling logic, but for what I needed in this non-critical script, it seemed as though it would suffice.
Well for some reason (and I've still not figured out why - please comment if you know) when get-process throws its exception, it's not trapped by my trap logic. It's definitely not entering the trap block; I added a "entering trap" line to see if it got in there, and it's not. Of course if I stick a throw "oops" statement in there, it enters the trap block just fine!
Soo, I thought perhaps I would try the more specific version of trap: trap [ExceptionType] {}. But I needed to figure out what type of exception was being thrown, and as Murphy would have it, the class of exception isn't displayed in the error output. So back to the chapter I went, and I found that most cmdlets have an “ea” argument that allows you to control how it behaves in the face of an exception. This turned to be an even easier solution to my problem, but I also learned some other stuff that I'll share here. I also learned about the $error variable, which is an array populated with errors as they occur:
get-process nosuchprocess -ea SilentlyContinue
$error
What I got from this was a whole laundry list of errors! Apparently the $error object at global scope collects all errors over time. I tried and was able to clear the list, which made things a little easier:
$error.clear
Now I could use the Exception property of the ErrorRecord class, which is the type you'll discover if you pipe $error into the get-member cmdlet:
$error.clear
get-process nosuchprocess -ea SilentlyContinue
$error[0].Exception | get-member
This was how I figured out that the type of exception I was looking for was InvalidOperationException (get-member tells you the type of the object at the beginning of it's output, which is convenient). To make a long story short, I tried trapping that particular error, but control still wasn't transferred to my trap handler. I'm still not sure what's up with that. But here was my final solution (UPDATE - I have a newer one, below):
function restartFoo {
$procList = get-process foo -ea SilentlyContinue
if (!$error) { $procList | stop-process }
c:\bin\foo.exe
}
One thing I learned about $error is that there appears to be a new version of it created in each scope. That is, the $error in my restartFoo function doesn't keep getting bigger over time as the one at global scope does. That makes it easy to test in a function as I've done above (this is how you test for null in PowerShell, as far as I can tell - there is no "null" keyword).
UPDATE (23 Jan, 2007): Looks like the above paragraph is wrong. My function started leaving copies of the process around, and it turned out this was because $error is indeed a single global variable that collects errors over time. I've posted my new solution here.
Anyway, I hope you find something useful here, even if it's just the free chapter(s) of the book. Be sure to buy a copy if you like it!
Posted
Jan 22 2007, 05:34 PM
by
keith-brown