I’m hardly an expert of PowerShell, but this topic seems to be greatly overlooked by the community. Most people seem to agree that the preferred way of error handling in PowerShell is via the try-catch-finally statement. The method is easy to use and it allows you to capture specific error types and perform a different action depending on the exception type. Here’s a simple example:
PS C:\> Try { Get-process non-existant-process -ErrorAction Stop } catch { Write-Host "Oh noes! You made a boo boo." -ForegroundColor DarkGray} Oh noes! You made a boo boo.
However, when it comes to using remote PowerShell with Exchange, things get ugly fast. Let’s run the above example with the Get-Mailbox cmdlet:
PS C:\> Try { Get-Mailbox non-existant-mailbox -ErrorAction Stop } catch { Write-Host "Oh noes! You made a boo boo." -ForegroundColor DarkGray} The operation couldn't be performed because object 'non-existant-mailbox' couldn't be found on 'DBXPR03A001DC01.EURPR03A001.prod.outlook.com'. + CategoryInfo : NotSpecified: (:) [Get-Mailbox], ManagementObjectNotFoundException + FullyQualifiedErrorId : [Server=DBXPR03MB617,RequestId=04c5f21e-b716-4a67-ac67-52f3c19af39c,TimeStamp=3/1/2015 2:52:47 PM] [FailureCategory=Cmdlet-ManagementObjectNotFoundException] B5614317,Microsoft.Exchange.Management.RecipientTasks.GetMailbox + PSComputerName : outlook.office365.com
The result is much different this time! Not only the ‘standard’ error message is displayed, but our custom error handling code is completely ignored. There are several reasons behind this, number one being the fact that you are not actually running the cmdlet – you are instead running a ‘proxy’ function in a implicit remoting session. See for yourself, get the definition of the Get-Mailbox cmdlet:
Get-Command Get-Mailbox | fl
OK, so how does knowing that small fact help us? It doesn’t, not really. It offers a workaround for the simplest of cases, and just that. Instead of using the proxy function, you can use Invoke-Command to ‘wrap’ it:
PS C:\> $ses = Get-PSSession | ? {$_.ConfigurationName -eq 'Microsoft.Exchange' -and ($_.Runspace.ConnectionInfo.ConnectionUri.Host -eq "outlook.office365.com") -and $_.Availability -eq "Available"} PS C:\> try { Invoke-Command -Session $ses -ScriptBlock { Get-Mailbox non-existant-mailbox } -ErrorAction Stop } catch {Write-Host "Oh noes! You made a boo boo." -ForegroundColor DarkGray} Oh noes! You made a boo boo.
Don’t get excited, this doesn’t make any difference. It all boils down to another small fact – the remote session you have opened with Exchange is run in ‘No Language‘ mode. This means no variables, no script blocks, no assignments, nothing at all, but the pipeline. No tab completion either. So while you can get the error handling to work when providing the identity of the mailbox as string, even passing a simple variable to the script block in Invoke-Command will not work due to the ‘No language’ mode. It’s completely useless.
Until the PowerShell team sits down with the Exchange team and give us a better option, don’t feel bad about not following the best practices when it comes to error handling.
Solution exists since, by passing variables as argument to -Argumentlist
See https://stackoverflow.com/questions/36339616/how-to-pass-local-variable-to-invoke-commands-scriptblock
try { Invoke-Command -Session $ses -ScriptBlock { Get-Mailbox $args[0] } -ArgumentList $UserUPN -ErrorAction Stop } catch {Write-Host “Oh noes! You made a boo boo.” -ForegroundColor DarkGray}
Nice Article,
Thanks for sharing, but they’re still one issue as I find, the Exception Category will remain System.Management.Automation.RemoteException
so capturing certain error is a bit challenging as all the errors will have the same category
Yup, but you can usually look at additional information and act accordingly. Sad thing is that “newer” cmdlets seem to ignore the good practices and all spill out some generic undistinguishable codes…
This is great!
I used to do stuff like
$OldErrorActionPreference = $Global:ErrorActionPreference
$Global:ErrorActionPreference =”SilentlyContinue”
If ($? -eq $False) {
}
$Global:ErrorActionPreference = $OldErrorActionPreference
But your solution is simple, elegant and just neat!!
Thanks so much for this article!
I’m on PowerShell 5.1 and found when I tried to do a Set-User and forgot to add ‘-ErrorAction:Stop’ I had to catch a different exception:
System.Management.Automation.Remoting.PSRemotingTransportException
Oh well, it’s been few years since I posted this, Microsoft has actually improved error handling for remote sessions quite a bit now 🙂
Thanks for the article! I found this while Googling for some hints on this same issue as I’ve been using some long-running scripts recently and struggling with EXO PowerShell error handling. I ended up realizing I could do the following successfully in PowerShell 5.1 (surely some earlier versions too):
try {Get-Mailbox asdfasdf -ErrorAction:Stop}
catch [System.Management.Automation.RemoteException] {“caught”}
And it works! I’ve done this before in on-premises Exchange Mgmt Shell, using a different type then this one [Syste….RemoteException]. It was a long time ago so I took a while to remember. Then it was just a matter of doing:
$Error[0].Exception.GetType().FullName
…to get the type name (after first running Get-Mailbox asdfasdf to generate the exception).
I also recently learned that Connect-ExoPSSession with MFA will survive authentication renewals seamlessly (for a while) if you specify -UserPrincipalName . I’ve dug into the Exo PS module and now have my own Connect-Exchange function using the same process as Connect-ExoPSSession.
Yup, that’s what I’ve been doing as well. Unfortunately, there are tons and tons of cmdlets that don’t have proper exceptions coded and return something generic.
The MFA module with -UserPrincipalName uses the PRT token afaik, so the machine must be joined to Azure AD. But it’s indeed useful/more reliable.
Do you know if this also works for Hybrid Exchange?
I tried the following:
Import-PSSession $session -DisableNameChecking -CommandName Get-RemoteMailbox
try {Get-RemoteMailbox asdfasdf -ErrorAction Stop}
catch [System.Management.Automation.RemoteException] {“caught”}
but it didn’t work.
I verified the exception type was the same with your $Error[0].Exception.GetType().FullName command.
Do I still need to set $ErrorActionPreference = ‘Stop’ before that code as well?
Thanks for this, really helped me get some code sorted!
# Simple demo:
try
{
Get-Mailbox -Identity bogusAlias -ErrorAction ‘Stop’
}
catch
{
Write-Host “This should trip, but it never does! The error action is ignored.”
}
# This works:
try
{
$Global:ErrorActionPreference = ‘Stop’
Get-Mailbox -Identity bogusAlias -ErrorAction ‘Stop’
}
catch
{
Write-Host “Exception caught!”
}
That was the simple fix for me. Thank you.
What I usually do is to put the result in a variable and check against it. For example if I’m looping over a list of users, I’d do something like:
$checkuser = Get-Mailbox $user -ErrorAction SilentlyContinue
if (!$checkuser) { Write-Error “User $user not found”; continue }
where of course you’d replace Write-Error with the appropriate action or remove it altogether if you don’t want to clutter the output with all the error messages.
eh, now try doing that on office365 with a “restricted language” active, meaning you CANNOT use any variables at all.
Nice article, but I’m personally stuck in trying to get the warning and error message from a “invoke-command -asJob ….” on a o365 exchange session
equivalent of a remote start-job, and tricky on the try-catch block…
And here I am thinking I’m doing something wrong with my Try-Catch for weeks… I’m trying to get the Mailbox properties for users in a CSV file & export the results (all is working fine), but those where the mailbox is not in the cloud, obviously got this ugly red error message all other the PS console.
At this point in time (Jan 2017), is there something we can do to log those errors in another file for users where the the mailbox is not in O365?
If not, is there a way to avoid displaying this error?
Thanks!
I’m just answering on “is there a way to avoid displaying this error?” : yes
“Get-Mailbox non-existant-mailbox -ErrorAction SilentlyContinue” will simply not display anything
(-ea for short instead of -ErrorAction, thers is also -warningaction)
if you pipe multiple command that could all give an error, you’ll need a “-ErrorAction” for each command
“get-mailbox -ea silentlycontinue |get-mailboxstatistics -ea silentlycontinue”
you also have a global variable $ErrorActionPreference that you can set to SilentlyContinue (but that’s not really a good idea most of the time.)