Error Handling and Resilience in PowerShell
Building Scripts with try/catch and error management like a pro
Introduction
Error handling is a critical skill for any PowerShell scripter. Whether you’re automating local tasks or interacting with remote APIs; being able to catch, analyze, and respond to errors will make your scripts more reliable and easier to troubleshoot. This post covers the theory and practice of PowerShell’s error handling, with real-world examples and troubleshooting tips for common API issues.
PowerShell try/catch Mechanics
PowerShell provides structured error handling using try, catch, and finally blocks. This lets you “catch” errors, inspect their details, and decide how your script should respond. It’s especially useful for preventing scripts from crashing and for providing helpful feedback to users.
Basic Example
Below, we intentionally trigger a division by zero error and use catch to handle it.
# Attempt a calculation that will fail
try {
# This will throw a 'divide by zero' error
$result = 10 / 0
Write-Host "Result: $result" # This line will not run due to the error above
}
catch {
# Output the error message to the user
Write-Host "An error occurred: $($_.Exception.Message)"
# Check if the error message contains 'divide by zero'
if ($_.Exception.Message -like '*divide by zero*') {
Write-Host "You tried to divide by zero! Please check your input values."
}
# You can add more logic here to handle other error types
}
In the example above, when we try to divide by zero, we will get an error. If you were to run this command from an interactive prompt, you would see a red text error message. In automation scenarios or when running a script file, those error messages may be suppressed or not visible. By using try/catch, we can capture the error and perform further actions, instead of letting the script fail silently or crash.
Finally Block
The finally block is optional and runs after the try and catch blocks, regardless of whether an error occurred. It’s useful for cleanup tasks.
try {
# This begins the try block
$TestChoice = Read-Host "Would you like to test an error? [y/n]"
If ($TestChoice -eq "y") {
Write-Host "Generating a test error..." -foregroundcolor Yellow
Throw "This is a test error"
}
} catch {
# This runs only if an error occurs
Write-Host "Caught an error: $($_.Exception.Message)" -ForegroundColor Red
} finally {
# This runs no matter what
Write-Host "Error test completed" -ForegroundColor Green
}
API Error Handling Example
When working with APIs, errors can occur for many reasons; network issues, invalid requests, or server-side problems. Using try/catch lets you capture these errors and extract useful information for troubleshooting.
Below, we purposely call a non-existent API endpoint to trigger an error. The code comments explain how to capture and log error details:
# Define the API endpoint (this one will fail)
$uris = @(
'https://api.example.com/endpoint-that-does-not-exist',
'https://swapi.dev/', #star wars api returns SSL error,
'https://www.deviantart.com/developers/', #deviantart api returns 403 error with details about cloudflare blocking
'https://www.searchapi.io/api/v1/search?engine=google&q=chatgpt', # returns invalid api key error
'https://api.enigma.com/v1/data', # returns missing token
'http://api.nobelprize.org/v1/prize.xml?year=2025' # returns 404 not found
)
foreach ($uri in $uris) {
try {
# Attempt to call the API
$response = Invoke-RestMethod -Uri $uri -Method Get
Write-Host "API call succeeded!"
}
catch {
# Inform the user that the API call failed
Write-Host "API call failed! - URI: $uri"
# Output the HTTP status code (e.g., 404, 500)
Write-Host "StatusCode: $($_.Exception.Response.StatusCode.value__)"
# Output the status description (e.g., Not Found)
Write-Host "StatusDescription: $($_.Exception.Response.ReasonPhrase)"
# Output the full error message
Write-Host "Error Message: $($_.errordetails.message)"
Write-Host "-----------------------------------"
}
}
In this example, we loop through a list of URIs that are expected to fail for various reasons. The catch block captures the error and extracts useful information such as the status code and error message, which can help diagnose the issue. Note that some APIs provide more error details than others, so the information available may vary. In most scenarios you will be working with a single API endpoint, so you can tailor the error handling to that specific API’s responses.
Common API Error Responses
APIs often return standard HTTP status codes to indicate what went wrong. Understanding these codes helps you quickly diagnose issues. Here are some of the most common errors and what they mean:
Typically, any status code starting with 200 indicates success. Status codes starting with 400 or 500 indicate errors.
400 Bad Request: The request was malformed (e.g., missing or extra properties, invalid JSON structure).
401 Unauthorized: Authentication failed or missing.
403 Forbidden: You do not have permission to access the resource.
404 Not Found: The endpoint or resource does not exist.
405 Method Not Allowed: The HTTP method used is not supported by the endpoint (using POST on a GET-only endpoint, for example).
415 Unsupported Media Type: The server cannot process the request payload format (e.g., sending XML when JSON is expected).
500 Internal Server Error: The server encountered an unexpected condition.
A full list of REST API error codes and descriptions can be found at https://restfulapi.net/http-status-codes/
Note: Many APIs return custom error codes and messages not covered here. Always consult the API documentation for specifics and troubleshooting tips.
Saving the JSON That Caused the Error
When an API call fails due to a bad payload, it’s important to save the exact JSON you sent. This makes it easier to debug and share with support teams or colleagues. Here we are going to intentionally create a failure and save the JSON payload that caused the error.
# Using the open free patch API
$uri = "https://api.jsonpatch.me/upload/"
# Create a complex PowerShell object to send as JSON
$jsonPayload = [PSCustomObject]@{
Name = "Alex Scriptshack"
Contact = [PSCustomObject]@{
Email = "alex@scriptshack.com"
Phone = "555-1234"
}
}
# Note that we DID NOT convert the object to JSON format. This will cause the API to reject the payload.
try {
# Attempt to send the payload to the API
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $jsonPayload -ContentType 'application/json'
# If it succeeds, inform the user
Write-Host "API call succeeded!"
}
catch { #Caught a failure
# Inform the user of the failure
Write-Host "API call failed. Saving payload for troubleshooting."
# Save the problematic JSON to a file for review
$uriwithnospecialcharacters = $uri -replace '[^a-zA-Z0-9]', '_'
$jsonPayload | Set-Content -Path "./${uriwithnospecialcharacters}_failed_payload.json"
# Save the error details to a log file
$errorDetails = $_ | Select-Object * | Out-String
$errorDetails | Set-Content -Path "./${uriwithnospecialcharacters}_api_error_log.txt"
}
Looking at the saved JSON file, we can see that the API rejected the payload because it was not in valid JSON format (error 400). To fix this, we need to convert the PowerShell object to JSON using ConvertTo-Json before sending it. How could that be handled within the error catch?
Finding and Resolving Specific Errors
APIs will respond to errors with status codes and messages that can help identify the problem. We can use these responses to build automatic fixes or custom actions into our catch blocks.
The simplest way to catch specific error messages is to check the status code returned in the error. In Powershell the status codes will be found in the exception response object. This can be accessed later using the $error automatic variable as well. Remember that $error[0] is the most recent error.
Using standard if/then logic, we can create custom handling for specific error codes. Let’s return to the example in the previous section, where we forgot to ConvertTo-Json before sending the payload. We can add logic to the catch block to handle that specific error.
# Using the open free patch API
$uri = "https://api.jsonpatch.me/upload/"
# Create a complex PowerShell object to send as JSON
$jsonPayload = [PSCustomObject]@{
Name = "Alex Scriptshack"
Contact = [PSCustomObject]@{
Email = "alex@scriptshack.com"
Phone = "555-1234"
}
}
# Note that we DID NOT convert the object to JSON format. This will cause the API to reject the payload.
try {
# Attempt to send the payload to the API
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $jsonPayload -ContentType 'application/json'
# If it succeeds, inform the user
Write-Host "API call succeeded!"
}
catch { #Caught a failure
# Inform the user of the failure
Write-Host "API call failed! Analyzing error..."
# Check the status code to identify the error using a switch statement
If ( $_.Exception.Response.StatusCode -Like '*') {
# There is a value in status code, let's use a switch to perform specific actions for specific entries.
switch ($_.Exception.Response.StatusCode.value__) {
# If a 400 is returned, we know the payload was bad
400 {
Write-Host "Bad Request - The payload may be malformed. Attempting to fix by converting to JSON."
# Convert the PowerShell object to JSON format
$jsonPayloadCorrected = $jsonPayload | ConvertTo-Json
try {
# Retry sending the corrected JSON payload
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $jsonPayloadCorrected -ContentType 'application/json'
Write-Host "API call succeeded on retry!"
}
catch {
Write-Host "Retry failed. Please check the payload and API documentation."
}
}
# If another status code is returned, log it for review
401 {
Write-Host "Unauthorized - Check your API credentials."
}
403 {
Write-Host "Forbidden - You do not have permission to access this resource."
}
404 {
Write-Host "Not Found - The endpoint does not exist."
}
500 {
Write-Host "Internal Server Error - The server encountered an error. Try again later."
}
# Catch-all for other status codes
default {
Write-Host "Unhandled error occurred. StatusCode: $($_.Exception.Response.StatusCode.value__)"
}
}
}
If ( -not $_.Exception.Response.StatusCode ) {
Write-Host "No status code returned. Possible network issue or non-HTTP error."
}
}
In the above example, we nested a second try/catch inside the first catch block to retry sending the corrected JSON payload if we received a 400 Bad Request error. This demonstrates how you can build resilience into your scripts by anticipating common errors and implementing automatic fixes. In the switch statement, another try/catch could be added for each specific error code to handle them as needed.
Troubleshooting Authentication Errors
Authentication errors (401 Unauthorized or 403 Forbidden) are common when working with APIs. These errors indicate that the API did not accept your credentials or that you do not have permission to access the requested resource. Here are some common causes and steps to resolve them:
Steps to Resolve Authentication Errors
Check Credentials: Make sure your API key, token, or username/password are correct and not expired.
Header Format: Confirm the
Authorizationheader is formatted as required (e.g.,Bearer <token>).Token Scope: Ensure your token has the necessary permissions (scopes) for the API endpoint.
Time Sync: Some APIs require your system clock to be accurate.
Consult API Docs: Review authentication requirements and error documentation.
Regenerate Credentials: If in doubt, generate a new API key or token.
Troubleshooting JSON Payloads
When sending data to an API, the structure and content of your JSON payload must match what the API expects. If your payload is incorrect, you’ll often get a 400 or 422 error. Here are some steps to help you troubleshoot:
Validate JSON Structure: Use online tools like JSONLint or VS Code’s built-in JSON validation to check for syntax errors.
Check Required Properties: Make sure all required fields are present and correctly named according to the API documentation.
Remove Extra Properties: Some APIs will reject requests with unknown or extra fields.
Match Data Types: Ensure property values match the expected types (string, number, boolean, etc.).
Consult API Docs: Always review the API’s schema and error documentation for guidance on required and optional fields.
Working Demonstration
Challenge time!
Create a PowerShell script that calls an API endpoint of your choice. (The list of error-prone URIs from earlier is a good starting point.)
Intentionally introduce an error (e.g., incorrect URL, missing authentication, malformed JSON payload).
Implement
try/catchblocks to handle the error.Extract and display or log useful error information (status code, message).
(Optional) Add logic to automatically fix the error if possible (e.g., retry with corrected payload).
Conclusion
Building resilient PowerShell scripts means anticipating errors and handling them gracefully. Use try/catch blocks to capture and analyze errors, log details for troubleshooting, and validate your data before sending it to APIs. With these techniques, you’ll spend less time debugging and more time building solutions that work reliably.
References:
