Leveling-up Your Scripts by Managing Errors
In a few of my recent articles I gave some examples of how you can use PowerShell to perform tasks in Microsoft 365 in a quicker and easier way. I have tonnes of PowerShell scripts for all types of requirements shared on my GitHub and they vary in levels of error management based on the topic they are focused towards.
In many of the articles I write about PowerShell, I intentionally leave out error handling because it can get in the way of showing the content the article is based on. Myself, and many others who write articles and provide code examples on specific topics such as Microsoft 365 will often add a disclaimer to highlight that error handling should be added. Similar to my previous article around adding a simple, effective log function to your scripts, I thought it might be good to explain what I mean when referring to adding error handling.
This will likely be a two-part series on multiple aspects of managing and avoiding errors in your PowerShell code. In this article, I’ll go through some of the key concepts to understand to help add take your scripts to the next level with error handling and other error management techniques. All the code I reference here is also available on GitHub here.
Side note: For the purposes of this article, I will be using PowerShell 7, if you are using older versions, some results may differ.
Error Actions
Before we get into handling errors, have you noticed that then a cmdlet in PowerShell fails, the majority of the time the script continues to run? I’ve seen scripts where this is expected and the big red text on screen is ignored. But what if the cmdlet that fails is a key part of the script and it’s relied on by everything else after it? Depending on where your error happens, passing by without action when there is an error can lead you to some pretty unexpeted behaviour.
We’ll look more at how to manage these errors later, but for now, let’s take the below short script. This two-line script should get a process on the system and then write the process name and ID to the screen.
$Process = Get-Process -Name "NoProcessWithThisNameExists"
Write-Host "The process ID for $($Process.Name) is $($Process.Id)"
When we run this though, no process is found from our Get-Process cmdlet, returning an error. By default, the script will continue but we are missing critical information for our next step. In this example we get an output that is missing content as shown in Figure 1 but let’s say you were deleting specific users from Microsoft Entra… Then missing the wrong error like this could put you in real bother.

The reason scripts continue on error is because of the default action specified in the $ErrorActionPreference variable. When your session starts up, this value is set to “Continue”, telling the script that if it hits an error, keep going.
We can change this behaviour by changing the ErrorAction. The available options are:
- Break – Enters debug mode when an error occurs. This is very useful when creating a script .
- Ignore – Completly ignores the error, it doesnt get recorded or saved in memory. There are very few use-cases for this one.
- Inquire – Prompts the user when an error happens to ask if they should continue. This is good when testing.
- Continue – The default behaviour, the error is logged to the screen and saved but the script keeps running.
- Stop – The error is logged to the screen and saved and the script terminates.
- SilentlyContinue – This is the same as continue except the error isn’t written to the screen. This is used quite a lot to prevent the red error text from showing in the console but probably not the best way to approach managing errors.
As we see above, there are multiple options available for what happens when an error occurs. We can set these options in two different ways. The first is that we change the $ErrorActionPreference variable at the start of our script as seen in the below code. This is great for setting the same action for all of your script.
$ErrorActionPreference = "Stop"
Another, more flexible option is to use the -ErrorAction paramete on your cmdlets:
Get-Process -Name "NoProcessWithThisNameExists" -ErrorAction Stop
We can also mix both methods. Let’s say I wanted to set the majority of the script to stop when an error occurs, but I also want one cmdlet to silently continue. In this case we set the ErrorActionPreference to “Stop” but on our cmdlet, use the -ErrorAction Parameter to tell that cmdlet to continue on error:
$ErrorActionPreference = "Stop"
Get-Process -Name "NoProcessWithThisNameExists" -ErrorAction silentlycontinue
Write-Host "I don't care if that last cmdlet failed"
Get-Process -Name "NoProcessWithThisNameExists"
Write-Host "The process ID for $($Process.Name) is $($Process.Id)"##When run in a script, this line will not be displayed
It’s important to understand the actions that are taken when an error occurs so that we can properly manage errors in the next step.
Try and Catch Errors
When we choose to stop on an error, we generally want to take some action like writing to a log file or maybe even using default values. To build logic for these scenarios we use a Try/Catch block. Try/Catch does exactly what it sounds like, it TRIES to do a task, and if that task fails, it will catch the error and do something else. A simple example, turning back to Microsoft 365, is if we were trying to find a specific user to update in our tenant, but the user doesnt exist. In this case, we probably don’t want to run all those update cmdlets.
We can use the below cmdlets where we retreive a user account and then write out the users display name to the console.
$user = Get-MgUser -UserId Userdoesntexist@seanmcavinue.net -ErrorAction Stop
Write-host "The users name is: $($user.DisplayName)"
Running this gives a messy output shown in Figure 2.

If we wrap this in a Try/Catch block, it looks like the below:
Try {
$user = Get-MgUser -UserId Userdoesntexist@seanmcavinue.net -ErrorAction Stop
Write-host "The users name is: $($user.DisplayName)"
}catch{
Write-Host "User doesn't exist!"
}
Now when we run this, the error get’s caught and we don’t have any of the mess, the script simply jumps to the catch where we are writing out a cleaner message (Figure 3).

Getting Exception Details
So far so good, we don’t have the messy output and can manage the outcomes of a failed cmdlet ourselves, but what if we actually want to know what the error we got during the Try block was?
We can actually access the details of the error that occurred through $_.Exception inside our catch block. For example, we can get the exception message by adding $_.Exception.Message to our catch as shown below:
Try {
$user = Get-MgUser -UserId Userdoesntexist@seanmcavinue.net -ErrorAction Stop
Write-host "The users name is: $($user.DisplayName)"
}catch{
Write-Host "User doesn't exist!"
Write-Host "By the way, the Error we got was: $($_.Exception.Message)"
}
Now we rerun our code as shown in Figure 4 and we can see we’ve captured the error, this is great for combining with a log function to keep track of where things go wrong.

Finally…
The last piece of the whole Try/Catch puzzle is “Finally”. So far, we have seen how we can stop our script from continuing if it gets to an error that might cause us problems. We also saw how to manage what happens when those problems occur.
Well what happens if we want to perform tasks after all these errors have happened. Going back to our example above, what if we wanted to disconnect from the Microsoft Graph at the end, regardless of if there was an error or not?
We could include the disconnection in multiple places in our script but that doesn’t exactly scale very well in the long run. Instead, we can use Finally! Finally does pretty much what it sounds like, after the Try has tried and the Catch has caught, the finally statement happens regardless of what happened before it.
Adding a Finally block to our code from before we have the below:
Try {
$user = Get-MgUser -UserId Userdoesntexist@seanmcavinue.net -ErrorAction Stop
Write-host "The users name is: $($user.DisplayName)"
}catch{
Write-Host "User doesn't exist!"
Write-Host "By the way, the Error we got was: $($_.Exception.Message)"
}Finally{
Disconnect-MgGraph
Write-Host "I don't really care if the last stuff worked, but I've logged you out!"
}
If we run this code we see that the error is handled and the tasks in the Finally block are carried out (Figure 5).

If the code worked and there was no error, the Finally block is still executed as shown in Figure 6:

Summary
In this article I’ve gone through the basics of adding error handling to your scripts. We covered how Try/Catch/Finally and Error Preferences are important to making sure your scripts don’t encounted unhandled errors leading to unexpected behaviours.
In the next part of this series, I’ll look at using the debugging functionaltiy when writing scripts and how you can perform your own validations when you need to get around potential errors before they occur.
