5

I've read quite a bit on powershell error handling and now I'm quite confused about what I should be doing on any given situation (error handling). I'm working with powershell 5.1 (not core). With that said: Suppose I have a module with a function that would look like this mock:

function Set-ComputerTestConfig {
  [CmdletBinding()]
  param(
    [Parameter(Position=0, Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string] $Name)

begin { ... }
process { 
 # task 1
 # task 2 => results in a failure that prevents further tasks
 # task 3
 # task 4
}
end { ... }

Let's say that for each computer name that I pass to this function, I have 4 tasks to complete, but if any of the tasks fail, I can't continue with the remaining tasks. How should I be producing an error (best practice) such that it halts "process" for this particular computer name but effectively continues to process the pipeline?

SynBiotik
  • 441
  • 1
  • 5
  • 14
  • Why just not working with a try catch block where all the four tasks are in a try block and the catching will continue on to the next computer? – JimShapedCoding Oct 23 '19 at 08:51

1 Answers1

8
  • If you want to continue processing inputs from the pipeline, you must emit a non-terminating error:

    • Write-Error writes non-terminating errors; it writes to PowerShell's error stream without generating an exception behind the scenes; execution continues normally.

      • If a .NET method call is the error source, as in your case, wrap it in try / catch, and call Write-Error -ErrorRecord $_ in the catch block:

        • try { <#task 1 #>; ... } catch { Write-Error -ErrorRecord $_ }
      • Unfortunately, still as of PowerShell Core 7.0.0-preview.4, Write-Error doesn't fully behave as expected, in that it doesn't set the automatic success-status variable, $?, to $false in the caller's context, as it should. The only workaround at present is to make sure that your function/script is an advanced one and to use $PSCmdlet.WriteError(); from a catch block you can simply use $PSCmdlet.WriteError($_), but crafting your own error from scratch is cumbersome - see GitHub issue #3629.

  • If you want processing to stop right away, use a terminating error:

    • throw creates terminating errors.
      • Unfortunately, throw creates a more fundamental kind of terminating error than binary cmdlets emit: unlike the statement-terminating errors emitted by (compiled) cmdlets, throw creates a script-terminating (fatal) error.

        • That is, by default a binary cmdlet's statement-terminating error only terminates the statement (pipeline) at hand and continues execution of the enclosing script, whereas throw by default aborts the entire script (and its callers).
        • GitHub issue #14819 discusses this asymmetry.
      • Again, the workaround requires that your script/function is an advanced one, which enables you to call $PSCmdlet.ThrowTerminatingError() instead of throw, which properly generates a statement-terminating error; as with $PSCmdlet.WriteError(), you can simply use $PSCmdlet.ThrowTerminatingError($_) from a catch block, but crafting your own statement-terminating error from scratch is cumbersome.

  • As for $ErrorActionPreference = 'Stop'

    • This turns all error types into script-terminating errors, and at least advanced functions / scripts - those expected to act like cmdlets - should not set it.

    • Instead, make your script / function emit the appropriate types of errors and let the caller control the response to them, either via the common -ErrorAction parameter or via the $ErrorActionPreference variable.

      • Caveat: Functions in modules do not see the caller's preference variables, if the caller is outside a module or in a different module - this fundamental problem is discussed in GitHub issue #4568.
  • As for passing errors through / repackaging them from inside your function script:

    • Non-terminating errors are automatically passed through.

      • If needed, you can suppress them with -ErrorAction Ignore or 2>$null and optionally also collect them for later processing with the -ErrorVariable common parameter (combine with -ErrorAction SilentlyContinue).
    • Script-terminating errors are passed through in the sense that the entire call stack is terminated by default, along with your code.

    • Statement-terminating errors are written to the error stream, but by default your script / function continues to run.

      • Use try { ... } catch { throw } to instead turn them into script-terminating errors, or ...

      • ... use $PSCmdlet.ThrowTerminatingError($_) instead of throw to relay the error as a statement-terminating one.

Further reading:

  • Guidance on when to emit a terminating vs. a non-terminating error is in this answer.

  • A comprehensive overview of PowerShell's error handling is in GitHub docs issue #1583.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    As a newb to review queues, I've been down the rabbit hole. I find that answers like this are by far the exception, and they should be called out. – WaitingForGuacamole Feb 25 '21 at 20:14
  • 1
    I always forget the syntax, here is an example: `$PSCmdlet.WriteError( [Management.Automation.ErrorRecord]::new( [Exception]::new("MyMessage"), 'MyErrorId', [Management.Automation.ErrorCategory]::InvalidData, $MyTargetObject ))` -- of course you should use more specific exception type if possible and also adjust the `ErrorCategory`. – zett42 Feb 03 '22 at 16:51