共用方式為


Trapping your favorite exceptions

Like most folks, I hate errors.

As a scripter, I hate seeing blood on the screen--to me, it means failure that I didn't anticipate.  When you're trying to put tools out there for other folks to use, nothing toasts your peer's or customer's confidence like a tool that doesn't fix itself or errors out without explanation.

Welcome to the world of Try/Catch/Finally.

The gist is simple:

 try { Do-Something }
catch [an exception] { Do-Something else, fix it, exit gracefully }
finally { regardless of error or successful completion, do this }

So, what's an exception look like? I'm glad you asked.  Usually, anytime you see the lovely PowerShell Red and the words "Exception" appear on the screen, you've probably caused an exception of some sort.

That, right there, is an exception.  In this case, I'm trying to retrieve details about my Azure AD Tenant using the Get-AzureADTenantDetail cmdlet.  The cmdlet, however, needs to be connected to an Azure AD tenant in order to work.  Since I'm not connected, it throws an exception, telling me just that.

The most recent error data is stored in the built-in variable $Error.  The $Error variable is an ArrayList object, so as your script and commands execute, any error received will be stored in place [0], and the older entries will be pushed down.  Merely typing $Error at the prompt will only display what you just saw, which isn't enough for us to really do anything with.  While we see the word exception, we need to know the name of what generated it in order to tell our script how to handle it.   You might be thinking "It's right there! AadNeedAuthenticationException!" Well, yes, but no.  We need to dig a little deeper to find what we want.

You can see what properties and methods are available in $Error by running $Error | Get-Member.

What we're trying to find right now is information on the type of exception.  The GetType() method appears to be available, but if you try to use it to get the detail you want, you'll be just retrieving the type information for the error variable.  Instead, we need to expand the exception itself, and see what methods and properties are available there:

We can then use the GetType() method on the exception to find out what type of exception we're trying to catch (although, the ultimate answer for this particular exception is already displayed in the previous screenshot, we're going to keep digging down to see what we find):

Getting closer.  We can see the "short" or "friendly" name of the exception, as referenced in the exception message that was originally output to the screen.  However, in order for catch to actually catch it, we need to specify the full name.  Fortunately, there's an attribute for that--and as you probably guessed, it's FullName.

Now, you may have seen that value a few times in the other screen shots.  The actual exception type name that we are trying to get is inside the FullName property; the cmdlet author just happened to use it in a few places.  I've seen plenty of cmdlets where the FullName is different than the name appearing in other areas, so just do your due diligence and drill down a few levels to save yourself potential headaches later.

With that data, we can now compose a try/catch block that will return a more friendly error to the user:

 try { $var = Get-AzureADTenantDetail } 
catch [Microsoft.Open.Azure.AD.CommonLibrary.AadNeedAuthenticationException] { Write-Host "You're not connected."}

If we run this, this is what we'll see a much better display:

You can place commands in the catch {} to perform certain actions, based on having activated it.  In this case, maybe I'd want to trigger the Connect-AzureAD cmdlet to run.  I can just as easily add it to that block, and I'm off to the races.

And finally--the finally block.  It's another block that can contain commands to execute.  Looking back at our pseudo code:

 try { Do-Something }
catch [an exception] { Do-Something else, fix it, exit gracefully }
finally { regardless of error or successful completion, do this }

We might decide to write to a log file, close a connection, or some other clean up task.  Any code that you want to execute regardless of whether the code in the try block completed successfully can be placed here.

So, what happens when we want to write code to catch multiple exceptions?  I'm so glad you asked.

Let's try something really simple that can generate a bunch of errors all at once--running Connect-AzureAD and then canceling the authentication dialog:

Lots of stuff to dig into.  We've got 4 errors to choose from.  I chose to display the exceptions by grouping the error message along with the exception type together, and separating them with a few blank lines:

 $Error.Exception | % { $_.Message; $_.GetType().FullName; "`n" }

By setting the ErrorAction to Stop, we can cause the command in the try block to issue a terminating error, and pass off to the catch block:

 Try { Connect-AzureAD -ErrorAction Stop}
Catch [Microsoft.Open.Azure.AD.CommonLibrary.AadAuthenticationFailedException] { "Authentication failed." }
Catch [Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException] { "It looks like someone canceled the login process." }
Catch [System.AggregateException] { "Anything else"}
Finally { "Thanks for playing!" }

Since the first error we encountered is treated as terminating, we'll drop into the catch block, match to the exception [Microsoft.Open.Azure.AD.CommonLibrary.AadAuthenticationFailedException], do what we have in that block, and then skip to the Finally block, where we write a bit more text to screen.

Go stomp your angry errors into submission!