Error Handling in PowerShell
Hello, World!
The meme introduction to just about every programming language that ever existed. Even full development frameworks make their default template something akin to running “Hello, World!” and aspiring developers are on their way. It’s time to hack and slash through strings, arrays, integers, and eventually duck tape a solution together.
The issue is that all to often we go from hello world right to making happy path code, and error handling becomes an obstacle instead of the scaffolding we can and should depend on. We create ways to stop errors from happening or debug with print statements in our code. For me personally, learning how bad my crutches were took years and wasted weeks/months of my life trying to fix code that — if I learned error handling — could have been used more productively.
Connie’s first script
Write-Output "Hello, World!" ---------------------------------- Hello, World!
There’s nothing wrong with this script. It’s not likely to error out or cause any issues. It really is as harmless as a hand wave. However, it’s about as helpful as waving your hand to to say hi to your blind neighbor. You may mean well, but you aren’t going to be building a deep relationship with your code…or your neighbor if you stay here.
Adding in actual functionality
$UserName = Read-Host "What is your name?: " Write-Output "Hello, $UserName!" -------------------------------------------- What is your name?: Connie Hello, Connie!
It’s simple. It takes in a response and prints it out as part of a greeting. Another trope, but at least we have something now. But, what if people pass us the wrong information? We probably want to address that in code.
No bad information!
$UserName = Read-Host "What is your name?: " if ($UserName -match "[\d!@#$%^&*()]") { Write-Host "That is not what we wanted! Give us a name." } Write-Output "Hello, $UserName" ------------------------------------------------ What is your name?: Connie1 That is not what we wanted! Give us a name. Hello, Connie1!
Well…that didn’t exactly work. We got the error message, and we also got the greeting which we didn’t want to get. How to fix…
Bespoke “Error Handling”
$UserName = Read-Host "What is your name?: " if ($UserName -match "[\d!@#$%^&*()]") { Write-Host "That is not what we wanted! Give us a name." } else { Write-Output "Hello, $UserName" } ------------------------------------------------ What is your name?: Connie1 That is not what we wanted! Give us a name.
This is common. You’ll start to if/else statements start to populate your code everywhere. You wonder how you can even begin to program without if/else in your logic. This magic system can handle anything…or just about. However, it turns out it’s pretty inefficient. Let’s explore that.
Adding more functionality
$UserName = Read-Host "What is your name?: " if ($UserName -match "[\d!@#$%^&*()]") { Write-Host "That is not what we wanted! Give us a name." } else { Write-Output "Hello, $UserName" } $Birthday = Read-Host "What is your birthday, $UserName?: " ------------------------------------------------ What is your name?: Connie1 That is not what we wanted! Give us a name. What is your birthday, Connie1?:
Well poop. Our data flowed through the application carrying the bad name through the code onto deeper code layers. We really needed the code to block that question if the original response was not great. So we start to nest.
Nesting like Russian Dolls
$UserName = Read-Host "What is your name?: " if ($UserName -match "[\d!@#$%^&*()]") { Write-Host "That is not what we wanted! Give us a name." } else { Write-Output "Hello, $UserName" $Birthday = Read-Host "What is your birthday, $UserName?: " if ($Birthday -Match "\d\d\d\d-\d\d-\d\d" ) { Write-Output "Totally righteous birthday, babe!" } else { Write-Output "Invalid birthday!" } } ------------------------------------------------ What is your name?: Connie Hello, Connie What is your birthday, Connie?: December 1, 1969 Invalid birthday!
We are now structuring our code in a nesting formation. Not only is this extremely difficult to read, it starts to become difficult to handle the different branches of the code as they intertwine. Additionally, these branches create additional work for the computer when they go to run your code which degrades performance with scale.
So, with that in mind, what does the average self taught programmer do? They skim (as fast as possible) the error handling pages of the language documents.
Try Catch, but with a Catch
$UserName = Read-Host "What is your name?: " try { if ($UserName -match "[\d!@#$%^&*()]") { throw "That is not what we wanted! Give us a name." } } catch { throw $_ } Write-Output "Hello, $UserName" # Get the user birthday $Birthday = Read-Host "What is your birthday, $UserName?: " try { if ($Birthday -Match "\d\d\d\d-\d\d-\d\d" ) { Write-Output "Totally righteous birthday, babe!" } else { throw "Invalid birthday!" } } catch { throw $_ } ------------------------------------------------ What is your name?: Connie1 Error: That is not what we wanted! Give us a name.
Whew! We got rid of the nesting, now we have error handling and things are great. Right? Well…not yet. There’s a couple of issues. Let’s lay them out:
By trying to catch at every function or logic step, you greatly decrease code readability, because it adds several lines we have to process repeatedly.
It creates repeat code that you can’t just modify in one place. Now every error handle has to be updated individually, which means when you add logging down the road you have a lot of tech debt to handle.
When you’re trying to add this in to existing code, it is exhausting.
Cleaner Handling
try { # Get the User Name $UserName = Read-Host "What is your name?: " if ($UserName -match "[\d!@#$%^&*()]") { throw "That is not what we wanted! Give us a name." } Write-Output "Hello, $UserName" # Get the User Birthday $Birthday = Read-Host "What is your birthday, $UserName?: " if ($False -eq ($Birthday -Match "\d\d\d\d-\d\d-\d\d") ) { throw "Invalid birthday!" } Write-Output "$Birthday is an amazing day to have been born!" } catch { # Global Error Handling Write-Error "Could not continue the script. $_" } finally { # Clean up tasks Write-Output "Exiting the script." } ------------------------------------------------ What is your name?: Connie1 Error: Could not continue the script. That is not what we wanted! Give us a name. Exiting the script.
Technically, this code is two lines of code longer than the previous example, but we added comments, clean script exiting, and global error handling for the whole script. It’s now a lot easier to handle any current and future errors with minimal overhead. All error handling is managed in the Catch block removing the need to chase down each catch block.
Now, I am a proponent of keeping functional code out of the main area of a script. Everything that’s processing/requesting data should be a self-contained function in my opinion. There’s definitely some personal preference here, but I’ll show why this is so helpful down the road.
Adding in functions
Function Get-UserName { $UserName = Read-Host "What is your name?: " if ($UserName -match "[\d!@#$%^&*()]") { throw "That is not what we wanted! Give us a name." } return $UserName } Function Get-Birthday { $Birthday = Read-Host "What is your birthday, $UserName?: " if ($False -eq ($Birthday -Match "\d\d\d\d-\d\d-\d\d") ) { throw "Invalid birthday!" } return $Birthday } try { # Get the User Name $UserName = Get-UserName Write-Output "Hello, $UserName" # Get the User Birthday $Birthday = Get-Birthday Write-Output "$Birthday is an amazing day to have been born!" } catch { # Global Error Handling Write-Error "Could not continue the script. $_" } finally { # Clean up tasks Write-Output "Exiting the script." } ------------------------------------------------ What is your name?: Connie1 Error: Could not continue the script. That is not what we wanted! Give us a name. Exiting the script.
Okay, so our simple script is getting a bit out of hand, I’ll admit, but do you really want to go through a hundred line sample to get to the same outcome? I didn’t think so.
With this new view, we’ve separated the business logic of the script from the data functions, which helps us better process what we want the script to do. The “logic” portion of the script has now dropped down to 17 lines of code vs the 21 lines when we had all the error handling done in line with the logic. Simplification can be very helpful when dealing with nasty bugs.
In addition to simplifying the business logic itself, we have increased readability. I can walk anyone through this code and explain what it is trying to do without having to gloss over the hairy details of the functions behind the logic. If there is an issue with the functions being called, it’s now a lot easier to identify the culprit and deal with the problematic code without interfering with the context of the business logic.
The long short
Knowing error handling and how it works in PowerShell is critical to your success as an engineer or developer. Without proper error handling, you are going to waste your time trying to build your own error handling logic that slows your app, bogs you down and makes debugging your code a nightmare. I hope that sharing my own progression through learning error handling (and how I eventually got better) will help.