Problem/Details
I am working in PowerShell and trying to figure out how custom Try Catch statements work. My current major issue involves mixing Try/Catch and If statements. So the idea of what I am trying to achieve is something like this:
try {
if (!$someVariable.Text) { throw new exception 0 }
elseif ($someVariable.Text -lt 11) { throw new exception 1 }
elseif (!($someVariable.Text -match '[a-zA-Z\s]')) { throw new exception 2}
}
catch 0 {
[System.Windows.Forms.MessageBox]::Show("Custom Error Message 1")
}
catch 1 {
[System.Windows.Forms.MessageBox]::Show("Custom Error Message 2")
}
catch 2 {
[System.Windows.Forms.MessageBox]::Show("Custom Error Message 3")
}
Now I know the above code is very inaccurate in terms of what the actual code will be, but I wanted to visually display what I'm thinking and trying to achieve.
Question
Does anyone know how to create custom error messages with PowerShell that could assist me with achieving something close to the above idea and explain your answer a bit? Thank you in advance
So far, the link below is the closest thing I have found to what I'm trying to achieve:
PowerShell Try, Catch, custom terminating error message
The Error you throw is stored at $_.Exception.Message
$a = 1
try{
If($a -eq 1){
throw "1"
}
}catch{
if ($_.Exception.Message -eq 1){
"Error 1"
}else{
$_.Exception.Message
}
}
I would suggest using the $PSCmdlet ThrowTerminatingError() method. Here's an example:
Function New-ErrorRecord
{
param(
[String]$Exception,
[String]$ExceptionMessage,
[System.Management.Automation.ErrorCategory] $ErrorCategory,
[String] $TargetObject
)
$e = New-Object $Exception $ExceptionMessage
$errorRecord = New-Object System.Management.Automation.ErrorRecord $e, $ErrorID, $ErrorCategory, $TargetObject
return $ErrorRecord
}
Try
{
If (not condition)
{
$Error = #{
Exception = 'System.Management.Automation.ParameterBindingException'
ExceptionMessage = 'Error text here'
ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
TargetObject = ''
}
$PSCmdlet.ThrowTerminatingError((New-ErrorRecord #Error))
}
} Catch [System.Management.Automation.ParameterBindingException]
{
'do stuff'
}
Related
I am working on a new PowerShell class that takes the path to an XML file in my old format and converts it to my new format. The constructor takes the path argument and calls a method, which should return a custom object with either the XML or an error message explaining the absence of the XML. What is odd is that the XML portion of the custom object is XML before the method returns, but immediately after it has been cast to a string.
So this class
class pxT_DefinitionsMigrater {
# Properties
$XML = [xml.xmlDocument]::New()
$Errors = [System.Collections.Generic.List[string]]::new()
# Constructor
pxT_DefinitionsMigrater ([string]$xmlPath) {
if ($oldDefinition = $this.readXMLFile($xmlPath).xml) {
$this.XML = $oldDefinition.xml
Write-Host "pxT_DefinitionsMigrater: $($oldDefinition.xml.GetType().FullName)"
} else {
$this.Errors = $oldDefinition.error
}
}
# Methods
[psCustomObject] readXMLFile ([string]$xmlPath) {
$readXMLFile = [psCustomObject]#{
xml = $null
error = $null
}
$fileStream = $null
$xmlreader = $null
$importFile = [xml.xmlDocument]::New()
$xmlReaderSettings = [xml.xmlReaderSettings]::New()
$xmlReaderSettings.closeInput = $true
$xmlReaderSettings.prohibitDtd = $false
try {
$fileStream = [io.fileStream]::New($xmlPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
$xmlreader = [xml.xmlreader]::Create($fileStream, $xmlReaderSettings)
$importFile.Load($xmlreader)
} catch [System.Management.Automation.MethodInvocationException] {
if ($_.Exception.Message -match ': "(?<string>.*)"$') {
$readXMLFile.error = "Error loading XML; $($matches['string'])" # removes 'Exception calling "Load" with "1" argument(s):' from message
} else {
$readXMLFile.error = "Error loading XML; $($_.Exception.Message)"
}
} catch {
$readXMLFile.error = "Error loading XML; $($_.Exception.FullName) - $($_.Exception.Message)"
} finally {
if ($xmlreader) {
$xmlreader.Dispose()
}
if ($readXMLFile.error) {
$readXMLFile.xml = $null
} else {
$readXMLFile.xml = $importFile
}
}
Write-Host "readXMLFile: $($readXMLFile.xml.GetType().FullName)"
return $readXMLFile
}
}
Will echo
readXMLFile: System.Xml.XmlDocument
pxT_DefinitionsMigrater: System.String
I am contemplating moving the XML load code into the ctor since that is the only place it is needed, but the fact that the XML is getting cast to string seems like something I need to understand. What is causing this? And what do I need to do to make this work with the method, should I want to?
I have a fairly large powershell scripts with many (20+) functions which perform various actions.
Right now all of the code doesn't really have any error handling or retry functionality. If a particular task/function fails it just fails and continues on.
I would like to improve error handling and implement retries to make it more robust.
I was thinking something similar to this:
$tries = 0
while ($tries -lt 5) {
try{
# Do Something
# No retries necessary
$tries = 5;
} catch {
# Report the error
# Other error handling
}
}
The problem is that I have many many steps where I would need to do this.
I don't think it make sense to implement the above code 20 times. That seems really superfluous.
I was thinking about writing an "TryCatch" function with a single parameter that contains the actual function I want to call?
I'm not sure that's the right approach either though. Won't I end up with a script that reads something like:
TryCatch "Function1 Parameter1 Parameter2"
TryCatch "Function2 Parameter1 Parameter2"
TryCatch "Function3 Parameter1 Parameter2"
Is there a better way to do this?
If you frequently need code that retries an action a number of times you could wrap your looped try..catch in a function and pass the command in a scriptblock:
function Retry-Command {
[CmdletBinding()]
Param(
[Parameter(Position=0, Mandatory=$true)]
[scriptblock]$ScriptBlock,
[Parameter(Position=1, Mandatory=$false)]
[int]$Maximum = 5,
[Parameter(Position=2, Mandatory=$false)]
[int]$Delay = 100
)
Begin {
$cnt = 0
}
Process {
do {
$cnt++
try {
# If you want messages from the ScriptBlock
# Invoke-Command -Command $ScriptBlock
# Otherwise use this command which won't display underlying script messages
$ScriptBlock.Invoke()
return
} catch {
Write-Error $_.Exception.InnerException.Message -ErrorAction Continue
Start-Sleep -Milliseconds $Delay
}
} while ($cnt -lt $Maximum)
# Throw an error after $Maximum unsuccessful invocations. Doesn't need
# a condition, since the function returns upon successful invocation.
throw 'Execution failed.'
}
}
Invoke the function like this (default is 5 retries):
Retry-Command -ScriptBlock {
# do something
}
or like this (if you need a different amount of retries in some cases):
Retry-Command -ScriptBlock {
# do something
} -Maximum 10
The function could be further improved e.g. by making script termination after $Maximum failed attempts configurable with another parameter, so that you can have have actions that will cause the script to stop when they fail, as well as actions whose failures can be ignored.
I adapted #Victor's answer and added:
parameter for retries
ErrorAction set and restore (or else exceptions do not get caught)
exponential backoff delay (I know the OP didn't ask for this, but I use it)
got rid of VSCode warnings (i.e. replaced sleep with Start-Sleep)
# [Solution with passing a delegate into a function instead of script block](https://stackoverflow.com/a/47712807/)
function Retry()
{
param(
[Parameter(Mandatory=$true)][Action]$action,
[Parameter(Mandatory=$false)][int]$maxAttempts = 3
)
$attempts=1
$ErrorActionPreferenceToRestore = $ErrorActionPreference
$ErrorActionPreference = "Stop"
do
{
try
{
$action.Invoke();
break;
}
catch [Exception]
{
Write-Host $_.Exception.Message
}
# exponential backoff delay
$attempts++
if ($attempts -le $maxAttempts) {
$retryDelaySeconds = [math]::Pow(2, $attempts)
$retryDelaySeconds = $retryDelaySeconds - 1 # Exponential Backoff Max == (2^n)-1
Write-Host("Action failed. Waiting " + $retryDelaySeconds + " seconds before attempt " + $attempts + " of " + $maxAttempts + ".")
Start-Sleep $retryDelaySeconds
}
else {
$ErrorActionPreference = $ErrorActionPreferenceToRestore
Write-Error $_.Exception.Message
}
} while ($attempts -le $maxAttempts)
$ErrorActionPreference = $ErrorActionPreferenceToRestore
}
# function MyFunction($inputArg)
# {
# Throw $inputArg
# }
# #Example of a call:
# Retry({MyFunction "Oh no! It happened again!"})
# Retry {MyFunction "Oh no! It happened again!"} -maxAttempts 10
Solution with passing a delegate into a function instead of script block:
function Retry([Action]$action)
{
$attempts=3
$sleepInSeconds=5
do
{
try
{
$action.Invoke();
break;
}
catch [Exception]
{
Write-Host $_.Exception.Message
}
$attempts--
if ($attempts -gt 0) { sleep $sleepInSeconds }
} while ($attempts -gt 0)
}
function MyFunction($inputArg)
{
Throw $inputArg
}
#Example of a call:
Retry({MyFunction "Oh no! It happend again!"})
Error handling is always going to add more to your script since it usually has to handle many different things. A Try Catch function would probably work best for what you are describing above if you want to have each function have multiple tries. A custom function would allow you to even set things like a sleep timer between tries by passing in a value each time, or to vary how many tries the function will attempt.
How to use 'throw' direction in PowerShell to throw an exception with custom data object?
Say, could it do this?:
throw 'foo', $myData
then the data can be used in 'catch' logic:
catch {
if ($_.exception.some_property -eq 'foo') {
$data = $_.exception.some_field_to_get_data
# dealing with data
}
}
edited:
My intention is to know is there a brief and cool syntax to throw an exception (without explicitly creating my own type) with a name that I can decide by its name and deal with its data in the 'catch' blocks.
You can throw any kind of System.Exception instance (here using a XamlException as an example):
try {
$Exception = New-Object System.Xaml.XamlException -ArgumentList ("Bad XAML!", $null, 10, 2)
throw $Exception
}
catch{
if($_.Exception.LineNumber -eq 10){
Write-Host "Error on line 10, position $($_.Exception.LinePosition)"
}
}
If you're running version 5.0 or newer of PowerShell, you can use the new PowerShell Classes feature to define custom Exception types:
class MyException : System.Exception
{
[string]$AnotherMessage
[int]$SomeNumber
MyException($Message,$AnotherMessage,$SomeNumber) : base($Message){
$this.AnotherMessage = $AnotherMessage
$this.SomeNumber = $SomeNumber
}
}
try{
throw [MyException]::new('Fail!','Something terrible happened',135)
}
catch [MyException] {
$e = $_.Exception
if($e.AnotherMessage -eq 'Something terrible happened'){
Write-Warning "$($e.SomeNumber) terrible things happened"
}
}
I'm trying to catch Exception calling a function running another function like this:
$ErrorActionPreference = "Stop"
function f {
$a = 1
$b = $a / 0
}
function Main($f) {
try {
$f
} catch [System.Exception] {
"Caught exception"
}
}
Main(f)
The problem is that the Exception is not caught and powershell show message like this:
Attempted to divide by zero.
In C:\test.ps1:4 car:5
+ $b = $a / 0
+ ~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : RuntimeException
Why the exception is not caught even if $ErrorActionPreference = "Stop" is on the top of code?
The problem is really at this line Main(f). What that does is call the function f and attempt to pass the result to the function Main. Of course before it can get the result there is an exception and the pipeline never gets into Main. You were trying to pass the function itself not the result. PowerShell doesn't have great ways to do this directly. It's a scripting language not a functional language. However you can declare the parameter to Main as a ScriptBlock and invoke it. A ScriptBlock is something that operates largely like a function object. Using it will have largely the same effect. However it's somewhat cumbersome to get a function's ScriptBlock. This should work as expected and is more standard style:
$ErrorActionPreference = "Stop"
function f {
$a = 1
$b = $a / 0
}
function Main {
param([ScriptBlock]$f)
try {
& $f
} catch [System.Exception] {
"Caught exception"
}
}
Main -f { f }
So instead we are passing a new scriptblock that just contains a call to f and invoking that wrapper.
Note that I've replaced the C-style function call syntax. In my experience that style is more trouble than its worth in PowerShell. Stick to the PowerShell style and things are often clearer.
If you really want to get the function's ScriptBlock, you need to do something like:
Main -f (get-command f).ScriptBlock
Well, you are not calling your Main function, you are calling the f function and passing the output of that as the input of the Main and then calling Main. Use Set-PSDebug and see.
Set-PSDebug -Trace 2
And testing now:
Main(f)
DEBUG: ! CALL function '<ScriptBlock>'
DEBUG: ! SET $ErrorActionPreference = 'Stop'.
DEBUG: 15+ >>>> Main(f)
DEBUG: 2+ function f >>>> {
DEBUG: ! CALL function 'f'
DEBUG: 3+ >>>> 1/0
While this returns 1.
function f {
1/1
}
function Main($f) {
try {
$f
} catch [System.Exception] {
"Caught exception"
}
}
Main(f)
And this catches as expected:
function f {
1/1
}
function Main($f) {
try {
$f/0
} catch [System.Exception] {
"Caught exception"
}
}
Main(f)
edit: To clarify the wrong second answer, this has nothing to do with precompilation, and its easily verifiable with the following code:
function f {
$a = 0
1/$a
}
function Main($input) {
try {
$input
} catch [System.Exception] {
"Caught exception"
}
}
Main(f)
I'm supposed to tag this as homework, but I'm not sure how. Not asking for an answer, just to be pointed in the right direction of what I'm doing wrong. Basically, this is supposed to make a complete sentence out of ANY user input, with the exception of -AllCaps, which is functioning properly. My problem is the other part.
param ($title, [switch]$AllCaps)
$ex="to|a|the|at|in|of|with|and|but|or"
function Proper($ts)
{
$nt=foreach($word in $ts)
{
$word=$word.ToLower()
if($word -match $ex)
{
if($word -eq $ts[0])
{
$letters=$word -csplit("")
$letters[1]=$letters[1].ToUpper()
$word=$letters -join("")
}
else
{
$word=$word
}
}
else
{
$letters=$word -csplit("")
$letters[1]=$letters[1].ToUpper()
$word=$letters -join("")
}
"$word"
}
$nt=$nt -join(" ")
Write-host $nt
}
if($AllCaps)
{
$title=$title.ToUpper()
"$title"
}
else
{
$ts=$title -split(" ")
$newtitle=Proper $ts
}
So, when I execute the script passing in "the waffle of kings", expected output is "The Waffle of Kings" - instead, the code seems to be completely ignoring the "else" in the first "if else" statement, and the output I'm getting is "The waffle of kings". Since "waffle" doesn't match anything in $ex, it should be moving to the else part, capitalizing the first letter.
Less of note is that it doesn't write to the console without including "$word" within the foreach loop, though the way I have it written, the Write-Host should be doing that for me. If I take out the write-host nothing changes, but if I take out the "$word" in the loop, I get no output at all.
Using the above mentioned
$ex='^(to|a|the|at|in|of|with|and|but|or)$'
I ended up with a good one-liner, but have expanded it out here:
Function Proper($words)
{
$ex='^(to|a|the|at|in|of|with|and|but|or)$'
$nt = $(
foreach ($word in $words)
{
$word = $word.ToLower();
if ($word -match $ex)
{
$word
} else
{
$word = ([String]$word[0]).ToUpper()+$word.Substring(1);
$word
}
}
) -join " "
return $nt
}
I hope that helps.
Thanks, Chris.