What's the right way to emit errors in powersh

2020-03-08 05:22发布

问题:

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?

回答1:

  • 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 this GitHub issue.

  • 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 error (runspace-terminating 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).
      • 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 this GitHub issue.
  • 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 runspace is terminated, 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 this GitHub docs issue.



回答2:

Use Try...Catch - make sure all commands use -ErrorAction Stop switch or set the environment to stop on error e.g. $ErrorActionPreference = 'Stop'

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


    [string] $Name)

begin { ... }
process {
    Try {
        # task 1
        # task 2 => results in a failure that prevents further tasks
        # task 3
        # task 4
    }
    Catch {
        Write-Host "One of the tasks has failed`nError Message:"
        Write-Host $_
    }
}
end { ... }