PowerShell Try/Catch and Retry
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
withStart-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
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.
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!"})