1
votes

I understand try-catch-finally in Powershell, but is there something similar to Python's 'else' clause, in which code runs only if there is not an error?

I'm writing a script that uses invoke-webRequest on a site that goes down frequently. I use try-catch to catch HTTP errors. I also want a block of code to run only if the invoke-webRequest command completes successfully.

try{
    invoke-WebRequest -URI 'http://flakywebsite.com/site1' -Method GET
    invoke-WebRequest -URI 'http://flakywebsite.com/site2' -Method GET
    invoke-WebRequest -URI 'http://flakywebsite.com/site3' -Method GET
    }
catch{
    "This line will execute only if there is an error in the try block"
    }
else{
    "This line will execute only if there is NOT an error in the try block"
    }
finally{
    "This code will run regardless of whether there is or is not an error in the try block."
    }

I have this workaround. It uses the catch block to append a notice to the $Error variable:

try{
    invoke-WebRequest -URI 'http://flakywebsite.com/site1' -Method GET
    invoke-WebRequest -URI 'http://flakywebsite.com/site2' -Method GET
    invoke-WebRequest -URI 'http://flakywebsite.com/site3' -Method GET
    }
catch{
    $Error.add("An error occurred in the try loop."}
    }
if($Error[-1] -ne "An error occurred in the try loop."){
    "This code will run only if there is NOT an error in the try block.
    }

This works, but it's awfully ugly. Is there a better way to do this in Powershell?

2
Maybe you can use if(!$Error){ "An error didn't occur" }.xyz
Any error occuring in Invoke-WebRequest should (depending on your $ErrorActionPreference ofc) trigger the switch to catch block, so why don't you just put the code from else block below Invoke-WebRequest?Robert Dyjas

2 Answers

3
votes

No, as of v7, PowerShell unfortunately does not offer a Python-like else clause as part of a try / catch statement.


One option is to place the only-if-not caught code at the bottom of your try block, after the commands that may fail - see Zafer Balkan's helpful answer.

If that is undesirable, I suggest the following idiom as a workaround:

# Make sure that even normally non-terminating errors trigger the
# `catch` block below.
# Note that the *first* error that occurs in the `try` block
# will jump to the `catch` block right away - subsequent commands won't execute.
$ErrorActionPreference = 'Stop'

# Save the count of errors currently recorded in the automatic $Errors collection.
$errCountBefore = $Error.Count

try {

    invoke-WebRequest -URI 'http://flakywebsite.com/site1' -Method GET
    invoke-WebRequest -URI 'http://flakywebsite.com/site2' -Method GET
    invoke-WebRequest -URI 'http://flakywebsite.com/site3' -Method GET

}
catch {
    # Write a custom error to the $Error collection, as the *first* 
    # (most recent) item.
    # Note: If you want the error to also *display*, use `-ErrorAction Continue`.
    Write-Error -ErrorAction SilentlyContinue "An error occurred in the try loop: $_"
}

if ($errCountBefore -eq $Error.Count) { # No errors occurred.
  "This code will run only if there is NOT an error in the try block."
}

Note: If you don't write an error in your catch block, use a custom [bool] variable such as $caught that you set to $true in your catch block, and then test for if (-not $caught).


As for what you tried:

$Error.add("An error occurred in the try loop."}

You shouldn't write directly to $Error (which makes the following points moot: don't add strings and don't append to the collection of errors it stores) for the following reasons:

$Error is a so-called automatic variable in PowerShell, which generally means that PowerShell itself manages it, and user code shouldn't modify it directly.

The only exception with respect to $Error is that you may want to clear the collection of errors that have accumulated in the session so far, using $Error.Clear()

Otherwise, PowerShell manages $Error as follows:

  • Any error is automatically logged in $Error, in reverse chronological order (most recent one first).

    • The only exceptions are commands that are invoked with -ErrorAction Ignore and stderr output from external programs.
  • $Error contains only error-record objects (instances of type System.Management.Automation.ErrorRecord) that provide structured, detailed information about each error.

Therefore, all your code needs to do in order to record errors in $Error is to either write an error to the error stream with Write-Error, or to throw a script-terminating error with the Throw statement - PowerShell will wrap each error in a System.Management.Automation.ErrorRecord instance, if necessary, and prepend to $Error in reverse chronological order.

1
votes

Well, in your try-catch you have added many different things. The best practices recommend one try block for each of your Invoke-Request commands, since you cannot get which of the sites created the error. But you can just add the line below your code.

try{
    invoke-WebRequest -URI 'http://flakywebsite.com/site1' -Method GET
    invoke-WebRequest -URI 'http://flakywebsite.com/site2' -Method GET
    invoke-WebRequest -URI 'http://flakywebsite.com/site3' -Method GET
    "This line will execute only if there is NOT an error in the try block"
    }
catch{
    "This line will execute only if there is an error in the try block"
    }
finally{
    "This code will run regardless of whether there is or is not an error in the try block."
    }