Search code examples
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:

    • (Non-module) functions (and script files) run in a child scope of the caller, so $currentFile = "$($LocalDirectory)$($_)" inside your upload-files creates a local $currentFile variable, that the caller doesn't know about, and which goes out of scope when existing the function.

    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:

    • You report that the nested call, to function upload-file (which isn't shown in the question) can generate a (statement-)terminating error, as a result of an exception thrown by a (WinSCP) .NET method call. Such a statement-terminating error is caught by an enclosing try { ... } catch { ... } statement, and control is instantly transferred to the catch block.
      Note that without the latter, such a statement-terminating error by default does not abort the enclosing function; see the last of the two links at the bottom for more information.
      Conversely, if you want continued execution despite using try { ... } catch { ... }, enclose the .NET method call too in try {...} catch { ... }, and translate the exception into a non-terminating error (try { ... } catch { $_ | Write-Error }), and combine that with method (c) below. (Of course, if you've ensured that no terminating errors can occur at all, you may dispense with try { ... } catch { ... } altogether.)

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

    • Non-terminating errors are far more common than terminating ones.

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

    • (a) Before calling it, (temporarily) set the $ErrorActionPreference preference variable to 'Stop', which promotes non-terminating errors to terminating ones.

      • Note: If your functions were defined in a module, this method would not work.
    • (b) Make your function an advanced (cmdlet-like) one, which makes it support the -ErrorAction common parameter , allowing you to pass -ErrorAction Stop on a per-call basis to achieve the same effect.

      • Decorating a function's param(...) block with a [CmdletBinding()] attribute is enough to make it an advanced one, and so is using a parameter-individual [Parameter()] attribute.

    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:

    • (a) and (b) by design abort processing (and transfer control to the catch block) once the first error occurs, whereas

    • (c) potentially runs the function call to completion, allowing potentially multiple, non-terminating errors to occur.


    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:

    • The about_Try_Catch_Finally help topic.

    • A description of the fundamental error types in the context of guidance for command authors on when to emit a terminating vs. a non-terminating error: this answer.

    • A comprehensive overview of PowerShell's surprisingly complex error handling: this GitHub docs issue.