PowerShell Try/Catch and Retry

29,879

Solution 1

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 {
                $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 2

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 3

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!"})
Share:
29,879
Brad
Author by

Brad

Updated on November 08, 2020

Comments

  • Brad
    Brad over 3 years

    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?