1

I'm writing a script to backup existing bit locker keys to the associated device in Azure AD, I've created a function which goes through the bit locker enabled volumes and backs up the key to Azure however would like to know how I can check that the function has completed successfully without any errors. Here is my code. I've added a try and catch into the function to catch any errors in the function itself however how can I check that the Function has completed succesfully - currently I have an IF statement checking that the last command has run "$? - is this correct or how can I verify please?

    function Invoke-BackupBDEKeys {

        ##Get all current Bit Locker volumes - this will ensure keys are backed up for devices which may have additional data drives
        $BitLockerVolumes = Get-BitLockerVolume | select-object MountPoint
        foreach ($BDEMountPoint in $BitLockerVolumes.mountpoint) {

            try {
            #Get key protectors for each of the BDE mount points on the device
            $BDEKeyProtector = Get-BitLockerVolume -MountPoint $BDEMountPoint | select-object -ExpandProperty keyprotector
            #Get the Recovery Password protector - this will be what is backed up to AAD and used to recover access to the drive if needed
            $KeyId = $BDEKeyProtector | Where-Object {$_.KeyProtectorType -eq 'RecoveryPassword'}
            #Backup the recovery password to the device in AAD
            BackupToAAD-BitLockerKeyProtector -MountPoint $BDEMountPoint -KeyProtectorId $KeyId.KeyProtectorId
            }
             catch {
                 Write-Host "An error has occured" $Error[0] 
            }
        }
    }     

#Run function
    Invoke-BackupBDEKeys

if ($? -eq $true) {

    $ErrorActionPreference = "Continue"
    #No errors ocurred running the last command - reg key can be set as keys have been backed up succesfully
    $RegKeyPath = 'custom path'
    $Name = 'custom name'
    New-ItemProperty -Path $RegKeyPath -Name $Name -Value 1 -Force
    Exit
}
 else {
    Write-Host "The backup of BDE keys were not succesful"
    #Exit
}
Russeller
  • 55
  • 7
  • 4
    `BackupToAAD-BitLockerKeyProtector` could throw an exception and `$?` would still return `$true` - your `catch` block is hiding this fact. If you want to handle errors further up the call stack, you'll need to re-throw the error (or throw a new one) – Mathias R. Jessen Jan 05 '22 at 17:27
  • 4
    Why not have all the code inside the same function, the content of the `if` statement could be on your _try_ statement and the `else` in the _catch_ statement. – Santiago Squarzon Jan 05 '22 at 17:31

2 Answers2

2
  • Unfortunately, as of PowerShell 7.2.1, the automatic $? variable has no meaningful value after calling a written-in-PowerShell function (as opposed to a binary cmdlet) . (More immediately, even inside the function, $? only reflects $false at the very start of the catch block, as Mathias notes).

    • If PowerShell functions had feature parity with binary cmdlets, then emitting at least one (non-script-terminating) error, such as with Write-Error, would set $? in the caller's scope to $false, but that is currently not the case.

    • You can work around this limitation by using $PSCmdlet.WriteError() from an advanced function or script, but that is quite cumbersome. The same applies to $PSCmdlet.ThrowTerminatingError(), which is the only way to create a statement-terminating error from PowerShell code. (By contrast, the throw statement generates a script-terminating error, i.e. terminates the entire script and its callers - unless a try / catch or trap statement catches the error somewhere up the call stack).

    • See this answer for more information and links to relevant GitHub issues.

  • As a workaround, I suggest:

    • Make your function an advanced one, so as to enable support for the common -ErrorVariable parameter - it allows you to collect all non-terminating errors emitted by the function in a self-chosen variable.

      • Note: The self-chosen variable name must be passed without the $; e.g., to collection in variable $errs, use -ErrorVariable errs; do NOT use Error / $Error, because $Error is the automatic variable that collects all errors that occur in the entire session.

      • You can combine this with the common -ErrorAction parameter to initially silence the errors (-ErrorAction SilentlyContinue), so you can emit them later on demand. Do NOT use -ErrorAction Stop, because it will render -ErrorVariable useless and instead abort your script as a whole.

    • You can let the errors simply occur - no need for a try / catch statement: since there is no throw statement in your code, your loop will continue to run even if errors occur in a given iteration.

      • Note: While it is possible to trap terminating errors inside the loop with try / catch and then relay them as non-terminating ones with $_ | Write-Error in the catch block, you'll end up with each such error twice in the variable passed to -ErrorVariable. (If you didn't relay, the errors would still be collected, but not print.)
    • After invocation, check if any errors were collected, to determine whether at least one key wasn't backed up successfully.

    • As an aside: Of course, you could alternatively make your function output (return) a Boolean ($true or $false) to indicate whether errors occurred, but that wouldn't be an option for functions designed to output data.

Here's the outline of this approach:

function Invoke-BackupBDEKeys {
  # Make the function an *advanced* function, to enable
  # support for -ErrorVariable (and -ErrorAction)
  [CmdletBinding()]
  param()

  # ...
  foreach ($BDEMountPoint in $BitLockerVolumes.mountpoint) {

      # ... Statements that may cause errors.
      # If you need to short-circuit a loop iteration immediately
      # after an error occurred, check each statement's return value; e.g.:
      #      if (-not $BDEKeyProtector) { continue }
  }
}     

# Call the function and collect any
# non-terminating errors in variable $errs.
# IMPORTANT: Pass the variable name *without the $*.
Invoke-BackupBDEKeys -ErrorAction SilentlyContinue -ErrorVariable errs

# If $errs is an empty collection, no errors occurred.
if (-not $errs) {

  "No errors occurred"
  # ... 
}
else {
  "At least one error occurred during the backup of BDE keys:`n$errs"
  # ...
}

Here's a minimal example, which uses a script block in lieu of a function:

& {
  [CmdletBinding()] param() Get-Item NoSuchFile 
} -ErrorVariable errs -ErrorAction SilentlyContinue
"Errors collected:`n$errs"

Output:

Errors collected:
Cannot find path 'C:\Users\jdoe\NoSuchFile' because it does not exist.
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thank you. i've been able to follow this one and have manipulated a little bit to adjust to how I understand however when including the $Error[0] in the catch i'm then unable to get this when running the function with -Error Variable $errors - this is blank and doesn't contain the errors in the catch block - see below: Invoke-BackupBDEKeys -ErrorAction SilentlyContinue -ErrorVariable $Errors that $Erros variable does not contain the errors from the catch block inside the function? please advise – Russeller Jan 06 '22 at 12:43
  • catch { # Convert the caught terminating error to a non-terminating one # and cotinue the loop. $Errors = Write-Error -Message "Error has occured" #Write-Error -Message "An error has occured backing up the BDY keys to AAD" } When running the function Invoke-BackupBDEKeys -ErrorAction Stop -ErrorVariable $Errors the erros are not returned in $Erros variable – Russeller Jan 06 '22 at 13:08
  • 1
    @Russeller, you must pass the variable name _without the `$`_ to `-ErrorVariable`. Don't use `-ErrorAction Stop` - it will abort your script as a whole and render `-ErrorVariable` useless. Please see my update - I've changed the approach to not use `try` / `catch` at all. – mklement0 Jan 06 '22 at 13:31
0

As stated elsewhere, the try/catch you're using is what is preventing the relay of the error condition. That is by design and the very intentional reason for using try/catch.

What I would do in your case is either create a variable or a file to capture the error info. My apologies to anyone named 'Bob'. It's the variable name that I always use for quick stuff.

Here is a basic sample that works:

$bob = (1,2,"blue",4,"notit",7)

$bobout = @{}                               #create a hashtable for errors

foreach ($tempbob in $bob) {
   $tempbob
   try {
      $tempbob - 2                          #this will fail for a string
   } catch {
      $bobout.Add($tempbob,"not a number")  #store a key/value pair (current,msg)
   }
}

$bobout                                     #output the errors

Here we created an array just to use a foreach. Think of it like your $BDEMountPoint variable.

Go through each one, do what you want. In the }catch{}, you just want to say "not a number" when it fails. Here's the output of that:

-1
0
2
5

Name                           Value
----                           -----
notit                          not a number
blue                           not a number

All the numbers worked (you can obvious surpress output, this is just for demo). More importantly, we stored custom text on failure.

Now, you might want a more informative error. You can grab the actual error that happened like this:

$bob = (1,2,"blue",4,"notit",7)

$bobout = @{}                               #create a hashtable for errors

foreach ($tempbob in $bob) {
   $tempbob
   try {
      $tempbob - 2                          #this will fail for a string
   } catch {
      $bobout.Add($tempbob,$PSItem)         #store a key/value pair (current,error)
   }
}

$bobout

Here we used the current variable under inspection $PSItem, also commonly referenced as $_.

-1
0
2
5

Name                           Value
----                           -----
notit                          Cannot convert value "notit" to type "System.Int32". Error: "Input string was not in ...
blue                           Cannot convert value "blue" to type "System.Int32". Error: "Input string was not in a...

You can also parse the actual error and take action based on it or store custom messages. But that's outside the scope of this answer. :)

Dharman
  • 30,962
  • 25
  • 85
  • 135
adamt8
  • 308
  • 1
  • 7