powershellvariablesscopetry-catch

Variables in Try-Catch blocks with Powershell?


I have been trying to write a script in powershell that renames a file if it does not get transferred correctly through WinSCP. The script looks something like this:

# Variables
$error = 0
$currentFile = "test"

# Functions
function upload-files {
    param(
        $WinScpSession,
        $LocalDirectory,
        $FileType,
        $RemoteDirectory
    )
    
    get-childitem $LocalDirectory -filter $FileType |
        foreach-object {
            # $_ gets the current item of the foreach loop. 
            write-output "Sending $_..."
            $currentFile = "$($LocalDirectory)$($_)"
            upload-file -WinScpSession $session -LocalDirectory "$($LocalDirectory)$($_)" -RemoteDirectory "$RemoteDirectory"
        }
}

try
{
    # Upload files
    upload-files -WinScpSession $Session -LocalDirectory [PathToLocalDirectory] -FileType [FileType] -RemoteDirectory [PathToRemoteDirectoy]
}    
catch
{
    Write-Host "Error: $($_.Exception.Message)"
    write-output $currentFile
    $errorMoveLocation = copy-item -path "$currentFile" -destination "$currentFile.err" -passthru
    Write-Host "Error File has been saved as $errorMoveLocation"
    $error = 1
}

I have removed the paths for ease of reading and some WinSCP lines that don't have anything to do with the issue.

When adding some script-breaking code after the upload-file function, it would go to the catch statement where I expected the $currentFile variable to be the $($LocalDirectory)$($_) since it was caught after setting the variable again. However, the actual value of the variable is the original "test" value that it was initiated with. I have tried changing the scope of $currentFile to both script, and global the same issue still happens. I am still relatively new at powershell and this is beyond my expertise. Any help would be greatly appreciated, thanks!


Solution

  • As written, there is indeed a variable scoping problem with your code:

    I have tried changing the scope of $currentFile to both $script:, and $global: the same issue still happens

    Using these scopes should work, given that you're explicitly specifying the target scope (and in the top-level scope, you don't even need the $script: scope specifier; also, $global: variables are best avoided, as they affect the global session state and linger after your script exits; while $script: is better than $global:, it is best to avoid cross-scope variable modification altogether.)

    The only explanation for $script:currentFile = ... not modifying the value in the script / caller's scope would be if get-childitem $LocalDirectory -filter $FileType produced no output, in which case the ForEach-Object script block is never entered.

    Also note that at least in your upload-files function there is no code that would generate a terminating error, which is the prerequisite for being able to use a try {...} catch { ... } statement.


    Ensuring the effectiveness of your try {...} catch { ... } statement:

    Update:

    Your upload-files function only emits non-terminating errors, whereas try / catch only catches terminating errors.

    To ensure that upload-files reports terminating errors, you have two options:

    Alternatively, (c) - assuming you've made your function an advanced one - you can use the common -ErrorVariable parameter to capture all the non-terminating errors in a self-chosen variable (e.g., -ErrorVariable errs). If you find this variable non-empty, you can then use throw to emit a (script-)terminating error that catch will handle (e.g., if ($errs) { throw $errs }

    Note that:


    Implementation of (a):

    $oldErrorActionPref = $ErrorActionPreference
    try
    {
        # Treat all errors as terminating
        $ErrorActionPreference = 'Stop'
        upload-files -WinScpSession $Session -LocalDirectory [PathToLocalDirectory] -FileType [FileType] -RemoteDirectory [PathToRemoteDirectoy]
    }    
    catch
    {
        Write-Host "Error: $($_.Exception.Message)"
        # ...
    }
    finally {
      $ErrorActionPreference = $oldErrorActionPref
    }
    

    Implementation of (b):

    # ... 
    
    function upload-files {
        [CmdletBinding()] # NOTE: This makes your function an *advanced* one.
        param(
            $WinScpSession,
            $LocalDirectory,
            $FileType,
            $RemoteDirectory
        )
    
        # ...
        
    }
    
    try
    {
        # Pass -ErrorAction Stop to treat all errors as terminating
        upload-files -ErrorAction Stop -WinScpSession $Session -LocalDirectory [PathToLocalDirectory] -FileType [FileType] -RemoteDirectory [PathToRemoteDirectoy]
    }    
    catch
    {
        Write-Host "Error: $($_.Exception.Message)"
        # ...
    }
    

    Implementation of (c):

    # ... 
    
    function upload-files {
        [CmdletBinding()] # NOTE: This makes your function an *advanced* one.
        param(
            $WinScpSession,
            $LocalDirectory,
            $FileType,
            $RemoteDirectory
        )
    
        # ...
        
    }
    
    try
    {
        # Pass -ErrorVariable errs to collect all non-terminating errors
        # that occur, if any.
        upload-files -ErrorVariable errs -WinScpSession $Session -LocalDirectory [PathToLocalDirectory] -FileType [FileType] -RemoteDirectory [PathToRemoteDirectoy]
        # If errors occurred, use `throw` to generate a (script-)terminating
        # error that triggers the `catch` block
        if ($errs) { throw $errs }
    }    
    catch
    {
        Write-Host "Error: $($_.Exception.Message)"
        # ...
    }
    

    See also: