PowerShell-Docs icon indicating copy to clipboard operation
PowerShell-Docs copied to clipboard

Our Error Handling, Ourselves - time to fully understand and properly document PowerShell's error handling

Open mklement0 opened this issue 6 years ago • 40 comments

The existing help topics that touch on error handling (about_Throw, about_CommonParameters, about_Preference_Variables, about_Trap, about_Try_Catch_Finally ):

  • have sown longstanding confusion due to conflating the two distinct types of terminating errors:

    • statement-terminating errors, as reported by cmdlets in certain non-recoverable situations (via the .ThrowTerminatingError() method) and by expressions in which a .NET exception / a PS runtime error occurs.
    • script-terminating errors (fatal errors), as either triggered by Throw or by escalating one of the other error types via error-action preference-variable / parameter value Stop.
  • have always contained the incorrect claim that the error-action preference / parameter values only pertain to non-terminating errors - which is true for the -ErrorAction parameter, but not the $ErrorActionPreference preference variable - see https://github.com/PowerShell/PowerShell/issues/4292

    • However, $ErrorActionPreference with SilentlyContinue and Ignore - which in isolation silences all errors, including terminating ones - is preempted by an enclosing try / catch statement or applicable trap statement. That is, in the presence of these constructs - only - $ErrorActionPreference in effect then does not apply to terminating errors, which are then still caught - see https://github.com/PowerShell/PowerShell/issues/19500#issuecomment-1515664895

    • Arguably, -ErrorAction too should apply to all error types (as appropriate), though changing hat would certainly be a breaking change - see https://github.com/PowerShell/PowerShell/issues/14819

It's time to:

  • correct the existing topics

  • provide a conceptual about_Error_Handling topic, as @GoateePFE suggests - see #1424

Below is my understanding of how PowerShell's error handling actually works as of Windows PowerShell v5.1 / PowerShell Core v7.3.4, which can serve as a starting point for about_Error_Handling, along with links to issues to related problems.

Do tell me if and where I got things wrong. The sheer complexity of PowerShell's current error handling is problematic, though I do realize that making changes in this area is a serious backward-compatibility concern.


  • Types of errors:

    • True PowerShell errors:

      • Non-terminating errors are issued by cmdlets or functions to signal failure with respect to specific inputs while continuing to process further (pipeline) input by default.

        • This allows potentially long-running commands to run to completion, despite partial failure. The errors reported can then be inspected via the error records collected in automatic variable $Error, allowing reprocessing of only the failed objects later (see below).

        • Note, however, that is possible for all input objects to cause nonterminating errors, amounting to complete failure overall.

        • Most cmdlet errors are non-terminating errors; e.g.: '/NoSuch', '/' | Get-Item reports an error for non-existent path /NoSuch, but continues processing with valid path /.

      • Terminating errors:

        • Important: In the context of remoting - whether explicit (e.g., via Invoke-Command) or implicit (e.g., via modules using implicit remoting) - terminating errors (of either kind) are converted to non-terminating ones.
        • Types of terminating errors:
          • Script-terminating (fatal) errors:

            • By default, they abort the entire enclosing script as well as any calling scripts. On the command line, a single statement or a list of statements submitted together can be thought of as an implicit script.
              • Only a try / catch or Trap statement can prevent hat.
            • The only way to directly trigger such an error is with the Throw keyword.
            • Note: Script-terminating errors are in effect fatal errors from the perspective of your code, if unhandled, - see https://github.com/PowerShell/PowerShell/issues/14819#issuecomment-786121832 for a technical discussion.
          • Statement-terminating errors are the (statement-level) counterparts to non-terminating errors: they terminate the enclosing statement (pipeline or expression) and are issued to signal that a statement encountered a problem that doesn't allow it to meaningfully start or continue processing.

            • Important:
              • Statement-terminating errors truly only terminate the statement at hand. By default, the enclosing script continues to run.
              • The statement that is terminated is only the immediately enclosing statement; therefore, for instance, a statement-terminating error that occurs in the (-Process) script block of a ForEach-Object call does NOT terminate the pipeline as a whole - see below for an example.
            • Situations in which statement-terminating errors occur:
              • PowerShell's fundamental inability to even invoke a command generates a statement-terminating runtime error, namely:
                • A non-existent command: nosuch -l # no such command exists
                • A cmdlet or function call with invalid syntax, such as incorrect parameter names or missing or mistyped parameter values: Get-Item -Foo # -Foo is an invalid parameter name Select-Object -First notanumber # -First expects an [int]
                  • Exception: If the mistyped value is bound via the pipeline, the resulting error is non-terminating see @alx9r's example.
                • Attempting to call another script that fails to parse (that is syntactically invalid).
              • While rare, cmdlet invocations that succeed syntactically can themselves generate statement-terminating errors, such as based on the contents of parameter values (not caught by parameter validation) or the state of the environment.
              • Strict-mode violations (such as trying to access a nonexistent variable when Set-StrictMode -Version 1 or higher is in effect).
              • Expressions can trigger statement-terminating errors too, namely:
                • via PowerShell expression runtime errors; e.g.: 1 / 0

                • via exceptions thrown by .NET method calls; e.g.: [int]::Parse('foo')

                • via statement-terminating errors that occurs inside a (...) subexpression (but not inside $(...) or @(...), which are independent statement contexts); e.g.: (Get-Item -Foo) + 'hi' is a statement composed of a single expression that is terminated as a whole; by contrast, $(Get-Item -Foo) + 'hi' is a statement composed of an expression and an embedded statement, so only the $(...) part is terminated - + 'hi' is still executed.

                  • As an aside: Better names for operators $() and @() would therefore have been [array] substatement operators rather than [array] subexpression operators.
    • Failures signaled by external utilities (command-line / console applications such as findstr.exe on Windows and terminal-based utilities such as awk on Unix) via their exit codes are non-terminating - in fact, external utilities reporting nonzero exit codes are not errors in a PowerShell sense.

      • Note that the only standardized information that a utility's exit code carries is whether it succeeded overall (exit code 0) or failed overall (nonzero exit code), with no further distinction.
        • The exit code of the most recently executed external utility is stored in the automatic $LASTEXITCODE variable - see below.
      • Stderr output from external utilities is NOT considered error output - see below.
  • Default error actions, logging and success indicators:

    • Default actions when errors occur:

      • a non-terminating error:

        • does not terminate the current statement (pipeline): processing continues with further input objects, if present.
        • is output to PowerShell's error stream (stream number 2), which in the console prints in red with detailed context information; preference variable $ErrorView can be used to change to a different format.
          • Note: The current default format is multi-line and "noisy", and there's a suggestion to change that: see https://github.com/PowerShell/PowerShell/issues/3647
        • is logged in the automatic $Error collection (see below)
      • a statement-terminating error:

        • terminates the current statement, but the script continues processing.
          • Important: The statement that is terminated is only the immediately enclosing statement; therefore, for instance, a statement-terminating error that occurs in the (-Process) script block of a ForEach-Object call does NOT terminate the pipeline as a whole - it only terminates the script-block invocation at hand.
            • Example: 1, 2 | ForEach-Object -Process { Get-Item -Foo } -End { 'pipeline ran to completion' }
              • The Get-Item -Foo calls cause statement-terminating errors, but the statements they terminate are the instances of the -Process script blocks, so pipeline processing continues, and pipeline ran to completion prints.
        • is output to PowerShell's error stream.
        • is logged in the automatic $Error collection.
      • a script-terminating error:

        • terminates the entire script
        • is output to PowerShell's error stream
        • is logged in the automatic $Error collection (which only matters if the session isn't terminated as a whole)
      • an external utility signaling failure by exit code / producing stderr output:

        • does not terminate the current statement (pipeline).
        • While stderr output is sent to PowerShell's error stream, it is not formatted like an error in the console, and by itself does not imply that an error occurred.
        • Stderr output is NOT logged in the automatic $Error collection
    • Default success indicators:

      • Automatic Boolean variable $? reflects whether the most recent statement, including calls to external utilities, experienced any error:

        • $True, if none, and $False, if at least one error occurred (because, in the case of non-terminating errors, multiple errors may have occurred).

        • In short: $? returning $False tells you only that some error occurred:

          • You cannot infer how many inputs experienced failure, which can range from 1 to all of them.
          • You cannot infer (from $? alone) whether a terminating error occurred or not.
        • Caveat: Because $? is set by every statement, its value is only meaningful immediately after the statement of interest.

        • $? is NOT set / set as expected in the following cases:

          • If a (by definition terminating) error was handled with a Try/Catch or Trap statement - unless the Catch handler is empty. (Non-terminating errors cannot be caught that way.)
            • With a Catch or Finally block present, the success of whatever statement appears last inside of them, if any, determines what $? is set to, with a (non-empty) Finally block taking precedence.
          • If the error is a non-terminating error and the command originating the error is enclosed in (...) - which turns the command into an expression - it is then the expression's own success that is reflected in $?: (Get-Item /NoSuch); $? yields $True, because the expression itself - whose sole purpose was to wrap the command - technically succeeded. (Get-Item -Foo); $? yields $False, because the statement-terminating error terminated the expression as a whole.
            • See https://github.com/PowerShell/PowerShell/issues/3359
      • When calling external utilities, automatic variable $LASTEXITCODE complements $? by containing the specific exit code set by the most recently executed external utility.

        • $? is set to $True if the utility's exit code was 0, and to $False otherwise.

        • Note that while $? is only meaningful immediately after the statement of interest, $LASTEXITCODE remains relevant until the next external-utility call is made; either way, however, it is preferable to save the value in another variable if it needs to be inspected later.

      • Note: Currently, PowerShell lacks operators that allow chaining of commands based on whether they indicate success or not, such as the && and || control operators in Bash:

        • A feature suggestion can be found here: https://github.com/PowerShell/PowerShell/issues/3241
    • Default logging:

      • Errors are logged in memory (for the duration of the session) in the global automatic $Error variable in reverse chronological order (most recent error first; i.e., $Error[0] refers to the most recent error):
        • $Error is a collection of type [System.Collections.ArrayList] and the errors are stored as error records of type [System.Management.Automation.ErrorRecord] that wrap the underlying .NET exception, which all errors ultimately are (instances of [System.Exception] or a derived type).

        • To inspect $Error items, pipe them to Format-List -Force (direct output would result in the same format as when the error originally occurred); e.g.: $Error[0] | Format-List -Force

        • You can clear the collection anytime with $Error.Clear()

        • Errors are NOT logged in the following circumstances:

          • When non-terminating errors occur in a cmdlet / advanced function to which -ErrorAction Ignore was passed (see below).

          • What external utilities print to stderr is not considered error output, therefore it is NOT logged in $Error (see below).

  • Modifying the default error actions and logging behavior:

    • PowerShell commands and expressions:

      • Via common parameter -ErrorAction or preference variable $ErrorActionPreference:

        • Per the documentation as of this writing, specifying an error action should only affect non-terminating errors.

          • While this is true with respect to the -ErrorAction common parameter, it is incorrect with respect to the $ErrorActionPreference preference variable, which affects terminating errors as well (see https://github.com/PowerShell/PowerShell/issues/4292).
            • Caveat: a try / catch statement at a higher level can apparently preempt $ErrorActionPreference - see https://github.com/PowerShell/PowerShell/issues/5693
          • Arguably, the -ErrorAction common parameter should affect terminating errors too, but changing that would be a major breaking change. - see https://github.com/PowerShell/PowerShell/issues/14819
          • In cmdlets (advanced scripts/functions) written in PowerShell code, an -ErrorAction value gets automatically translated into a script/function-local $ErrorActionPreference value, which with -ErrorAction SilentlyContinue can result in throw statements getting ignored and unwanted code execution - see https://github.com/PowerShell/PowerShell/issues/19500#issuecomment-1508924277
        • In the context of remoting, terminating errors (of either type) are converted to non-terminating ones.

          • Caveat: When invoking functions from a module that uses implicit remoting, neither -ErrorAction nor $ErrorActionPreference work as expected:
            • -ErrorAction is applied remotely, which means that whatever errors occur is invariably converted to a non-terminating error locally.
            • The caller's $ErrorActionPreference value (unless it happens to be the global variable value) is not seen by the implicitly remoting module (which is typically an auto-generated script module), because modules have their own namespaces that only inherit from the global scope. The current inability of a script module to opt into the caller's preferences (without cumbersome workarounds) is discussed here: https://github.com/PowerShell/PowerShell/issues/4568
        • The supported action values are:

          • Continue ... non-terminating errors only: output errors and log them, but continue processing the current statement (non-terminating errors).
          • Stop ... non-terminating errors and statement-terminating errors via $ErrorActionPreference only: escalate the error to a script-terminating one.
          • SilentlyContinue ... like Continue, but silence error output, while still logging errors. Via $ErrorActionPreference only, also applies to both types of terminating errors (processing continues).
          • Ignore (-ErrorAction parameter only) ... non-terminating errors only: like SilentlyContinue, but without logging errors in $Error. Due to Ignore not being supported via $ErrorActionPreference, n/a to terminating errors.
            • Due to a bug, you can currently set $ErrorActionPreference to Ignore, even though you shouldn't be able to: https://github.com/PowerShell/PowerShell/issues/4348
            • Due to another bug, passing -ErrorAction Ignore to an advanced function currently causes a spurious statement-terminating error - see https://github.com/PowerShell/PowerShell/issues/1759
          • Inquire ... prompt the user for the desired action, including the option to temporarily enter a nested session for debugging. Via $ErrorActionPreference only, also applies to both types of terminating errors.
          • Suspend (workflows only) ... automatically suspends a workflow job to allow for investigation.
        • Ad-hoc, when calling cmdlet/advanced functions, you can pass common parameter -ErrorAction to modify the behavior of non-terminating errors (only!).

          • As stated, the -ErrorAction parameter has no impact on terminating errors - in line with documented, but debatable behavior.
        • Scope-wide (including descendant scopes, unless overridden there), you can set preference variable $ErrorActionPreference, which sets the scope's default behavior for all occurrences of non-terminating behaviors, and - against documented behavior - also for terminating errors.

          • Caveat: Advanced functions defined in script modules do not see the caller's preference variables by default, and making them do so is currently quite cumbersome - see https://github.com/PowerShell/PowerShell/issues/4568

          • As stated, cmdlets invoked via an implicit-remoting modules also do not see the caller's preference variables.

        • The -ErrorAction parameter takes precedence over the $ErrorActionPreference variable.

        • Non-terminating errors:

          • may alternatively be silenced with 2>$null, analogous to silencing stderr output from external utilities (see below). Terminating errors ignore 2>$null
            • Note that error output suppressed with 2>$null or redirected to file with 2>$file is nonetheless still recorded in the $Error collection, by design - see https://github.com/PowerShell/PowerShell/issues/4572
          • may additionally be collected command-scoped in a user variable, via common parameter -ErrorVariable, unless -ErrorAction Ignore is also used. Note that use of -ErrorVariable does not affect the error output behavior; e.g., with error action Continue in effect, non-terminating errors still print to the console, whether or not you use -ErrorAction.
      • Catching terminating errors with Try / Catch or Trap statements:

        • Important: Only terminating errors (of either type) can be caught this way, and Try / Catch and Trap are effective irrespective of the current $ErrorActionPreference value (and any -ErrorAction common parameter, which fundamentally only applies to non-terminating errors).

        • Inside a Catch block:

          • Automatic variable $_ contains the [System.Management.Automation.ErrorRecord] instance representing the terminating error at hand.
          • You can use just Throw (without an argument) to re-throw the error at hand.
        • You may define multiple Catch blocks by applying optional filters based on the underlying .NET exception types - see Get-Help about_Try_Catch_Finally.

        • Error logging: Errors caught this way are still logged in the $Error collection, but there is debate around that - see https://github.com/PowerShell/PowerShell/issues/3768

    • External utilities:

      • Given that an external utility signaling failure by returning a nonzero exit code is not a true PowerShell error:

        • A failure signaled this way cannot be escalated to a script-terminating error via the $ErrorActionPreference variable.

        • Error messages (printed to stderr) are formatted like regular output and are NOT logged in the automatic $Error collection variable, nor do they affect how automatic variable $? is set (that is solely based the utility's exit code).

          • The rationale is that external utilities, unlike PowerShell commands, have only two output streams at their disposal: stdout for success (data) output, and stderr for everything else, and while error messages are printed to stderr, other non-data output (warnings, status information) is sent there too, so you cannot make the assumption that all stderr output represents errors.
      • However, you can capture stderr output for later inspection:

        • Using output redirection 2> allows you to suppress stderr output or send it to a file: whoami invalidarg 2>$null suppresses stderr output; whoami invalidarg 2>err.txt captures stderr output in file err.txt

          • Caveat: Due to a bug still present as of PowerShell Core v6.0.0-beta.5, when you use any 2> redirection (redirection of PowerShell's error stream):

            • Stderr lines are unexpectedly collected in the $Error collection, despite the redirection, and even though stderr output is by default NOT collected in $Error.

            • This can accidentally trigger a script-terminating error if $ErrorActionPreference = 'Stop' happens to be in effect.

            • See https://github.com/PowerShell/PowerShell/issues/4002

        • There is currently no convenient mechanism for collections stderr lines in a variable, but, as a workaround, you can use redirection 2>&1 to merge stderr output into PowerShell's success output stream (interleaved with stdout output), which allows you to filter out the stderr messages later by type, because PowerShell wraps the stderr lines in [System.Management.Automation.ErrorRecord] instances; e.g.:

          • $stdout, $stderr = ($(whoami; whoami invalidarg) 2>&1).Where({ $_ -isnot [System.Management.Automation.ErrorRecord] }, 'Split'); you can then convert the $stderr array to strings with $stderr = $stderr | % ToString.
          • https://github.com/PowerShell/PowerShell/issues/4332 proposes adding a feature for directly collecting stderr lines in a variable, using a syntax such as 2>&var
  • Reporting custom errors in functions and scripts:

    • Guidelines for when to use non-terminating errors vs. statement-terminating errors in the context of creating cmdlets are in MSDN topic Cmdlet Error Reporting; a pragmatic summary is in this Stack Overflow answer of mine.

    • Non-terminating errors:

      • The Write-Error cmdlet is designed to generate non-terminating errors; e.g.: Write-Error "Not an existing file: $_" wraps the message in a [System.Management.Automation.ErrorRecord] instance and outputs it to the error stream.
      • However, Write-Error currently neglects to set $? to $False in the caller's scope, the way a compiled cmdlet does when reporting a non-terminating error - see https://github.com/PowerShell/PowerShell/issues/3629
        • Thus, after calling a script or function, $? may mistakenly contain $True even when Write-Error calls were made.
        • The workaround - available in advanced functions only (those whose param() block is decorated with the [CmdletBinding()] attribute) - is to use the $PSCmdlet.WriteError() method - see example below.
    • Terminating errors:

      • Statement-terminating errors:

        • PowerShell has NO keyword- or cmdlet-based mechanism for generating a statement-terminating error.

        • The workaround - available in advanced functions only - is to use $PSCmdlet.ThrowTerminatingError():

          • Note: Despite the similarity in name with the Throw keyword (statement), this truly only generates a statement-terminating error.
          • See example below.
        • On a side note: Sometimes it is desirable to terminate upstream cmdlets, without terminating the pipeline as a whole, the way Select-Object -First <n> does, for instance. As of Windows PowerShell v5.1 / PowerShell Core v6.0.0-beta.5, there is no way to do this, but such a feature has been suggested here: https://github.com/PowerShell/PowerShell/issues/3821

      • Script-terminating errors:

        • Use the Throw keyword.
          • Any object can be thrown, including none. In the context of a Catch handler (as part of a Try / Catch statement), not specifying an object causes the current exception to be re-thrown.
          • Unless the object thrown already is of type [System.Management.Automation.ErrorRecord], PowerShell automatically wraps the object in an instance of that type and stores the object thrown in that instance's .TargetObject property.

Example use of $PSCmdlet.WriteError() in an advanced function so as to create a non-terminating error (to work around the issue that Write-Error doesn't set$? to $False in the caller's context):

# PSv5+, using the static ::new() method to call the constructor.
# The specific error message and error category are sample values.
& { [CmdletBinding()] param()  # quick mock-up of an advanced function

  $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
      # The underlying .NET exception: if you pass a string, as here,
      #  PS creates a [System.Exception] instance.
      "Couldn't process this object.",
      $null, # error ID
      [System.Management.Automation.ErrorCategory]::InvalidData, # error category
      $null) # offending object
    )
}

# PSv4-, using New-Object:
& { [CmdletBinding()] param()  # quick mock-up of an advanced function

  $PSCmdlet.WriteError((
    New-Object System.Management.Automation.ErrorRecord "Couldn't process this object.",
      $null,
      ([System.Management.Automation.ErrorCategory]::InvalidData),
      $null
  ))

}

Example use of $PSCmdlet.ThrowTerminatingError() to create a statement-terminating error:

# PSv5+, using the static ::new() method to call the constructor.
# The specific error message and error category are sample values.
& { [CmdletBinding()] param()  # quick mock-up of an advanced function

  $PSCmdlet.ThrowTerminatingError(
    [System.Management.Automation.ErrorRecord]::new(
      # The underlying .NET exception: if you pass a string, as here,
      #  PS creates a [System.Exception] instance.
      "Something went wrong; cannot continue pipeline",
      $null, # error ID
      [System.Management.Automation.ErrorCategory]::InvalidData, # error category
      $null  # offending object
    )
  )

}

# PSv4-, using New-Object:
& { [CmdletBinding()] param()  # quick mock-up of an advanced function

  $PSCmdlet.ThrowTerminatingError((
    New-Object System.Management.Automation.ErrorRecord "Something went wrong; cannot continue pipeline",
      $null, # a custom error ID (string)
      ([System.Management.Automation.ErrorCategory]::InvalidData), # the PS error category
      $null # the target object (what object the error relates to)
  ))

}

mklement0 avatar Aug 15 '17 21:08 mklement0

@juanpablojofre this would make a great about topic

SteveL-MSFT avatar Aug 16 '17 15:08 SteveL-MSFT

https://github.com/PowerShell/PowerShell/issues/3629

zjalexander avatar Oct 11 '17 20:10 zjalexander

A note about ThrowTerminatingError, either regression from v2 or some not clear design change.

In the following script, ThrowTerminatingError is not caught in v5 and v6. It is caught in v2.

[CmdletBinding()]
param()

$caught = 'not-caught'
try {
    # this is not caught in v5 (works in v2)
    $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ([Exception]'some-error'), $null, 0, $null))
}
catch {
    $caught = 'was-caught'
    throw
}
finally {
    $caught
    'in-finally'
}

Copied from https://github.com/nightroman/PowerShellTraps/tree/master/Basic/ThrowTerminatingError/Catch-is-not-called

nightroman avatar Dec 20 '17 19:12 nightroman

@mklement0 I see your recommendation to utilize $PSCmdlet.WriteError() instead of write-error to fix the issue with $? but it looks like these have different behaviors when the "ErrorActionPreference" of the script/module is set to "stop".

Im testing this in Powershell 5.1

If erroractionpreference = stop and you use $PSCmdlet.WriteError() it appears to produce a locally non-terminating error but when it leaves the function it appears to be terminating. That error is not catch-able in the function producing the error but is catche-able by the caller.

If erroractionpreference = stop and you use write-error it appears to produce a statement terminating error which can be caught by the function.

$ErrorActionPreference="stop"
$VerbosePreference="continue"

function Test-WriteError {
    [CmdletBinding()]
    Param( )
    try {
        $errorRecord = New-Object Management.Automation.ErrorRecord (([Exception]'some-error'), $null, 0, $null)
        $PSCmdlet.WriteError($errorRecord) # This does not get caught when erroraction is set to stop
    } catch {
        write-verbose "Caught"
    }
}


function Test-Write-Error {
    [CmdletBinding()]
    Param( )
    try {
        write-error "Test" #This gets caught if ErrorActionPreference is Stop or -erroraction Stop is passed to this command
    } catch {
        write-verbose "Caught"
    }
}

Running Test-WriteError will terminate the script:

PS > test-writeerror; write-host "print"
some-error
At line:1 char:1
+ test-writeerror; write-host "print"
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Test-WriteError], Exception
    + FullyQualifiedErrorId : Test-WriteError

Running Test-Write-Error will not terminate the script:

PS > test-write-error; write-host "print"
VERBOSE: Caught
print

In other words, $PSCmdlet.WriteError does not appear to throw a terminating error when $ErrorActionPreference = Stop whereas write-error does

strawgate avatar Jan 13 '18 02:01 strawgate

@strawgate: Thanks for pointing out that difference - I have no explanation for it.

To me, your discovery suggests that one currently should always use $PSCmdlet.WriteError() instead of Write-Error (which, needless to say, is very unfortunate):

While you could argue that technically Write-Error exhibits the correct behavior - with Stop in effect, writing a non-terminating error should throw an exception - pragmatically speaking, the $PSCmdlet.WriteError() behavior is much more sensible:

You don't want to catch your own attempts with try / catch inside an advanced function to report a non-terminating error. Instead, you want the caller to handle this, as implied by the calling context's $ErrorActionPreference value or the -ErrorAction value passed to the advanced function (which function-internally is translated into a local-scope $ErrorActionPreference variable reflecting that value).

There is no good solution to this problem with Write-Error:

  • if you used Write-Error -ErrorAction Continue, your own catch handler wouldn't get triggered, but execution would continue, which is undesired.

  • The only solution is to reissue the Write-Error from the catch block, which is obviously cumbersome and awkward.

mklement0 avatar Jan 21 '18 14:01 mklement0

@mklement0 Thank you very much for your excellent post. It has been invaluable to me for making sense of a number of things.

Script Terminating Errors vs Exceptions

Is there a difference between "script-terminating errors" and exceptions? There seems to be places in the PowerShell project that distinguish between "script-terminating errors" and exceptions. I haven't, however, been able to observe a difference between them. Is there a difference?

What's the "Script" in "Script-Terminating Error"?

What is "script" meant to refer to in "script-terminating errors"? It seems like "script-terminating errors" often (almost always in my use of PowerShell) do something other than terminate a script. It seems like the only case where the stack unwinding caused by a "script-terminating error" stops at something that would be called a "script" is where there happens to be nothing that catches the "script-terminating error". Am I missing something? Is there some other definition of "script" that applies to "Script-Terminating Error"?

.ThrowTerminatingError does not a Statement-Terminating Error Make

The original post includes the following statements:

Statement-terminating errors truly only terminate the statement at hand. By default, the enclosing scripts continues to run. ... PowerShell has NO keyword- or cmdlet-based mechanism for generating a statement-terminating error. The workaround - available in advanced functions only - is to use $PSCmdlet.ThrowTerminatingError():

Consider, however, that

function e {
    try { 1 }
    catch{ Write-Host 'catch upstream' }
}

function f {
    param ( [Parameter(ValueFromPipeline)]$x )
    process {
        # nonexistentcommand
        $PSCmdlet.ThrowTerminatingError(
            [System.Management.Automation.ErrorRecord]::new(
                'exception message',
                'errorId',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $null
            )
        )
    }
}

try
{
    e | f
    Write-Host 'statement following statement with .ThrowTerminatingError'
}
catch
{
    Write-Host 'outer catch'
}

outputs outer catch. If it were true that "statement-terminating errors truly only terminate the statement at hand" and .ThrowTerminatingError() caused a statement-terminating error, it seems to me that the output would have been statement following statement with .ThrowTerminatingError.

alx9r avatar Feb 03 '18 17:02 alx9r

@alx9r: Thanks for the nice feedback and digging deeper. I'll need more time to give your post the attention it deserves, but let me say up front that it was I who came up with the term "script-terminating", out of necessity, given that the existing docs made no distinction between the sub-types of terminating errors.

Thus, there's nothing authoritative about this term, and if it turns out to be a misnomer, we should change it.

mklement0 avatar Feb 07 '18 13:02 mklement0

@alx9r:

Disclaimer: Little of what I state below has been arrived at through source-code analysis. All readers are welcome to point out any misinformation.

Is there a difference between "script-terminating errors" and exceptions?

From what I understand, all PowerShell errors (as opposed to stderr output from external utilities) are exceptions under the hood. A script-terminating error is simply an exception that isn't caught by PowerShell.

What's the "Script" in "Script-Terminating Error"?

Presumably, the ~~more technically accurate term would be runspace-terminating error, but the term runspace is not a term a PowerShell(-only) user is necessarily expected to be familiar with~~. [Update: It's hard to come up with a succinct, technically accurate term, though calling such errors fatal is a pragmatic alternative - for the technical underpinnings, see https://github.com/PowerShell/PowerShell/issues/14819#issuecomment-786121832]

In practice, what I call a script-terminating error, when uncaught:

  • aborts a running script, including its callers.

  • aborts a runspace created with [powershell]::Create().

  • aborts a single command line submitted at the command prompt - even if composed of multiple, ;-separated statements. (Presumably, such a command line runs in a runspace created ad hoc).

Again, the most descriptive term is open to debate. Session-terminating error is perhaps an alternative, but that also could be confusing with respect to behavior at the command prompt.

If it were true that "statement-terminating errors truly only terminate the statement at hand" and .ThrowTerminatingError() caused a statement-terminating error, it seems to me that the output would have been statement following statement with .ThrowTerminatingError.

In this context, the pipeline as a whole is the statement, even though a pipeline is itself by definition composed of sub-statements (an expression or command as the 1st segment, and commands as the subsequent segments).

Again, the terminology is open to debate: If an actual pipeline is involved, you could argue that pipeline-terminating error is the better choice, but note that that doesn't apply to expression-only statements such as 1 / 0 that also generate a statement-level-only error - it is for that reason that I stuck with statement as the qualifier.

Another example to demonstrate the pipeline behavior of a statement-terminating error:

# Advanced function that generates a statement-(pipeline-)terminating error
# when the input object is integer 2
function f {
  param ([Parameter(ValueFromPipeline)] [int] $i)
  begin { write-host 'before2' }
  process {
    if ($i -eq 2) {
      $PSCmdlet.ThrowTerminatingError(
        [System.Management.Automation.ErrorRecord]::new(
            'exception message',
            'errorId',
            [System.Management.Automation.ErrorCategory]::InvalidOperation,
            $null
        )
      )
    }
    $i
  }
  end { write-host 'after2' }
}

# Send an 3-element array through the pipeline whose 2nd element triggers the statement-terminating error.
1, 2, 3 | % { write-host 'before1' } { $_ } { write-host 'after1' } | f

The above yields:

before1
before2
1
f : exception message
...

That is, the entire pipeline was instantly terminated when the statement-terminating error was generated (no further input objects were processed, and the end blocks didn't run).

mklement0 avatar Feb 17 '18 23:02 mklement0

Since I came across this post months ago I've been trying to arrive at empirical proof of the various statements in the OP about "terminating", "non-terminating", "script-terminating", and "statement-terminating" errors. Currently I am doubtful that there exists definitions of those terms that would result in a useful taxonomy of PowerShell errors.

I have found few useful generalizations about PowerShell errors that stand up to testing. Most hypotheses involving generalizations and nuance (including some in OP) can be disproven with a small amount of testing. The truth about PowerShell errors seems remarkably resistant to simplicity.

I am fairly confident, however, that the following generalizations about PowerShell errors are true:

  1. A PowerShell expression or statement might do all, some subset, or none of the following error-related actions: a. write records to the $global:Error variable b. write records to a non-global copy of $Error c. write records to a variable named in an -ErrorVariable argument d. output to PowerShell's error stream e. update the value of $? f. terminate a pipeline g. terminate a statement h. throw an exception
  2. Whether each of the actions in (1) occurs depends at least on one or more of the following: a. the value of $ErrorActionPreference b. the value passed to -ErrorAction or the value of $ErrorAction c. the presence of an enclosing try{} block (eg. PowerShell/PowerShell#6098) d. the precise way an error is reported (eg. Write-Error vs. $PSCmdlet.WriteError() vs. throw $PSCmdlet.ThrowTerminatingError())

More nuanced generalizations would, of course, be useful. Currently, however, I am skeptical that such generalizations can be found and proven. Indeed my attempts at doing so have seemed sisyphean.

From my perspective the useful takeaway from this exercise is currently the following:

  • there is little that can be known without testing about which error actions any given PowerShell expression or statement takes
  • test plans should be informed by the possible actions in (1) and predicate conditions in (2)

alx9r avatar Feb 25 '18 00:02 alx9r

@alx9r:

My writing the OP had multiple intents:

  • I wanted to point that the current documentation is hopelessly inadequate.

  • I wanted to provide a pragmatic summary of the de-facto behavior, so as to help us poor souls who have to live with and make some sense of the current behavior.

    • In the course of doing so, I coined terms that made sense to me where none were to be found in the docs.

    • While I don't doubt that my summary doesn't cover all nuances - it was arrived at empirically as well, not through studying source code - my hope was that it covers most aspects and gets at least the fundamentals right.

    • Believe me, I understand that you ran out of steam trying while investigating, but if you have pointers as to where my summary is fundamentally off, please share them.

  • I wanted to start a conversion about cleaning up the byzantine mess that PowerShell error handling currently is - save for linking to various issues highlighting specific problematic behaviors, I didn't do a good job of making that explicit, however.

    • Sadly, with the release of v6 - which had many breaking changes - an opportunity was missed to radically simplify PowerShell's error handling in favor of something that is consistent and fit's into a user's head.

mklement0 avatar Feb 25 '18 19:02 mklement0

@mklement0 I think we are on the same page in striving for a better record of PowerShell error behavior.

I wanted to provide a pragmatic summary of the de-facto behavior, so as to help us poor souls who have to live with and make some sense of the current behavior.

As far as I'm concerned your OP succeeds at this. And I am very thankful that you wrote it.

While I don't doubt that my summary doesn't cover all nuances - it was arrived at empirically as well, not through studying source code - my hope was that it covers most aspects and gets at least the fundamentals right.

I think your OP is probably as close to "fundamentally" correct as I've seen. The problem is that there are so many exceptions and nuances to the "fundamental" behavior that "knowing" the "fundamentals" is not particularly useful.

...if you have pointers as to where my summary is fundamentally off, please share them.

The truth of PowerShell errors is sufficiently messy that I am not convinced that there are objective fundamentals that your summary could be "off" from.

I think it is probably useful for me to continue to report repros of newly-surprising error handling behavior. I'm not sure what the best venue for that is, but when I encounter behavior that is sufficiently inscrutable, I expect to continue reporting it to the PowerShell/PowerShell repository.

I wanted to start a conversion about cleaning up the byzantine mess that PowerShell error handling currently is..

It seems to me that making PowerShell errors less byzantine would involve numerous breaking changes. I don't think that would be a good way forward. It seems like we are mostly stuck with the current PowerShell error behavior.

alx9r avatar Feb 25 '18 23:02 alx9r

@alx9r:

I don't think that would be a good way forward. It seems like we are mostly stuck with the current PowerShell error behavior.

It certainly wouldn't be a good way forward in Windows PowerShell for reasons of backward compatibility, but I'm still holding out hope for PowerShell Core... (the official releases so far are an unhappy mix of making long-time Windows users unhappy due to breaking changes while carrying forward enough warts to make long-time Unix users stay away, if I were to guess).

mklement0 avatar Feb 26 '18 00:02 mklement0

It seems we need add statement/expression/pipeline definitions.

iSazonov avatar Feb 26 '18 17:02 iSazonov

@iSazonov:

Indeed. Let me take a stab at it (again, not arrived at by studying the source code - do tell me where I'm wrong):

  • The smallest unit of execution is a statement.

  • A statement is made up of the following statement subtypes, either in isolation or in combination:

    • an expression; e.g. , 1 + 2 or 'a', 'b'

    • a pipeline, which involves one or more commands:

      • a command invokes an executable - be it a cmdlet, a function, an alias, or an external utility; e.g., Get-ChildItem /
      • a single command by itself is a pipeline too, despite the absence of the | symbol (see previous example)
      • a pipeline with at least 2 segments may start with an expression, but all other segments must be commands; e.g., 1, 2 | Measure-Object

In the context of a single statement, (...) can be used to compose a statement from the two subtypes; aside from enforcing precedence in an expression (e.g., (1 + 2) * 3), (...):

  • allows you to convert a single pipeline to an expression so as to make it part of a larger expression, e.g., (Get-ChildItem / | Measure-Object -Sum Length).Sum -gt 1mb; note that the embedded pipeline is then not a statement in its own right, but a part of the overall statement; also note that a pipeline converted this way is executed in full, up front, with all its output collected in an array.

  • allows you to pass an expression as a command argument (e.g., Get-Date -Day 1 -Month ([datetime]::Now.month)).

By contrast, operators & {...}/ . {...} and $(...) / @(...) allow you to nest statements, i.e., embed one - or more - statements in a statement, and all embedded statements are statements in their own right with respect to statement-terminating errors.
More generally, a script block passed to another command for execution (e.g., ForEach-Object) is its own statement context(s).
The same applies to the script blocks that make up group statements (higher-order statements), by which I mean keyword-based flow-control constructs, such as loops (foreach/for, do, while), conditionals (if, switch) and exception handlers (try ... catch ... finally, trap); by contrast, a statement-terminating error in the conditional of such a group statement does terminate the entire construct (e.g., if (1/0) { 'yes' } else { 'no' }).

  • $(...) and @(...) create an expression from the embedded statements, which, again, means that all statements are executed to completion up front and their output is collected in an array.

  • & {...}/ . {...} create a command from the embedded statements, which means that output from the embedded statements is passed through the pipeline one by one, as it is being produced.

To illustrate the difference between using (...) to form a part of the enclosing statement and a nested statement with respect to statement-terminating errors (using $(...) in the example, but it applies equally to @(...) and analogously to . { ... } / & { ... }):

  • "Result: " + (Get-Item -NoSuchParameter) is a single statement due to the use of (...), which the statement-terminating error provoked with Get-Item -NoSuchParameter terminates as a whole: the only output is the error output.

  • "Result: " + $(Get-Item -NoSuchParameter) or the equivalent "Result: $(Get-Item -NoSuchParameter)", by contrast, due to the use of $(...), are nested statements, with the statement-terminating error only terminating the embedded statement: the string-literal part is still output, after the error.

mklement0 avatar Feb 26 '18 22:02 mklement0

Situations in which statement-terminating errors occur:

  • PowerShell's fundamental inability to even invoke a command generates a statement-terminating runtime error, namely: ...
    • A cmdlet or function call with invalid syntax, such as incorrect parameter names or missing or mistyped parameter values: Get-Item -Foo # -Foo is an invalid parameter name

This is consistent with my experiments, however, there is more nuance to this as follows:

  • failed binding of parameters provided on the command line causes a terminating error
  • failed binding of parameters provided from the pipeline causes a non-terminating error

Here is a repro of the terminating and non-terminating errors cause by failed command-line and pipeline parameter binding, respectively:

$ErrorActionPreference = 'Continue'
function f {
    param ( [Parameter(ValueFromPipeline,Mandatory)][int]$x )
    process { Write-Host $x }
}

Write-Host '=== Command-line binding error'

f -x 'not an int'
Write-Host 'That caused a command-line binding error causes a statement-terminating error...'

try
{
    f -x 'not an int'
}
catch
{
    Write-Host '...which is caught by a catch block.'
}

Write-Host '=== Pipeline binding error'

try
{
    1,2,'not an int',3 | f
    Write-Host 'That was a pipeline-binding error, which was non-terminating.'
}
catch
{
    Write-Host 'This catch block is not run because the pipeline binding error is non-terminating'
}

alx9r avatar Mar 01 '18 16:03 alx9r

Good catch (accidental pun), @alx9r. I've update the OP, which now links to your comment.

mklement0 avatar Mar 02 '18 15:03 mklement0

This is the best reference for PowerShell error handling I've come across on the web. Would love to see more formal documentation with this type of info! Thanks!

pstephenson02 avatar Feb 11 '19 21:02 pstephenson02

Seems like we are missing an opportunity to fix the error handling mess as part of a move to Powershell Core (6) .... There will likely never be another major shift in powershell which changes the EXE name and much of the underlying structure.

jtmoree-github-com avatar Mar 14 '19 15:03 jtmoree-github-com

Thanx for the insightfull article. I can confirm that in Azure Runbooks there is indeed a difference between throw and $PSCmdlet.ThrowTerminatingError($_) in line with your distinction between script and statement terminating error. In runbooks you would always distinguish non-terminating and terminating errors as Stream Errors and Exceptions:

  • A Stream Error is shown as Error in the runbook Job History, but the runbook status would still be Completed (not showing errors unless you log these with Log Analytics or dig for them in the job history).
  • An Exception is shown as Error in the runbook Job History, and under Exception, and the runbook status would be Failed

however if you (as i have seen many runbook authors do) use try/catch pattern with ThrowTerminatingError() inside an adv function, this will NOT trigger the runbook to have a failed state (wich one would rightfully expect as the name hints). This is of course because the script is not terminated, only the adv function you called:

`function Test-ExceptionHandling { [cmdletbinding()] param () process { try { Write-Output "Process start try" throw "this is terminating.... !" Write-Output "Process end try" } catch { Write-Output "Process start catch" $PSCmdlet.ThrowTerminatingError($_) # does NOT throw a terminating error! Write-Output "Process end catch" } } }

Test-ExceptionHandling`

If you replace $PSCmdlet.ThrowTerminatingError($) with throw $ the runbook will fail

AndersRask avatar Aug 21 '19 13:08 AndersRask

@mklement0 I even thank you very much for your excellent post. @all thank you for the high quality discussion.

I came here on my research of PowerShell event Logging (especially Log to text file) Logging and PowerShell is a very sad story ...

So I miss Words about the use and pitfalls of -ErrorVariable

You have showed up many quirks and byzantine things with PowerShell Error handling.

So many PowerShell Users may be, very confused how to set up an Error handling in PowerShell that has an reliable expected behavior.

So how to give users a guardrail for error handling?

In short,

In the past I gave my students the advice to allways use Advanced Functions and inside them to use the following construct in the Process Block (similar to be used in Begin{} and End{}).

  • allways put your whole code inside a big Try{}/Catch{} Block
  • inside directly after the Try { put the Statement $ErrorActionPreference = 'Stop' to make this work in the Try scope
  • inside the Catch {} use Write-Error $Error[0] to re-throw the Error. Write-Error is used here to respect the callers preference and second to have control over the terminating behavior you can then use Write-Error -ErrorAction 'Stop' to re-throw. $Error[0] is used instead of $_ to have self describing code and i heard rumor that $_ can have an unexpected Value inside a Catch block.

The following example of an advanced Function block worked for me in the past.

Notice: A Try{} block creates NO new scope, the $ErrorActionPreference = ... statement inside a Try{} block would NOT be confined to the Try{} block !

    Process {
        
        ForEach ($Item in $InputObject) {
            
            Try {
                $ErrorActionPreference = 'Stop'

                # Example to call a cmdlet 
                Get-Item -Path 'C:\nosuch'
                
                # Code here
                 # Code here
                  # Code here
                   # ...
            
            
            } Catch {
                # do error handling here

                # possibly re-throw the Error
                Write-Error $Error[0]  # -ErrorAction 'Stop'
            
            } # Finally {}
        }
    }

So the question with this code is What to use and what not?

The following assumption is inncorect

~~Using $PSCmdlet.WriteError($) ? Really? What if the $ Variable contains an Exception and not an $ErrorRecord? This will fail.~~ So i Stay with Write-Error

~~or to make an cumbersome switch inside the Catch block?~~

Warning! Incorrect code snippet here!

Catch {
    If($_ -is [System.Management.Automation.ErrorRecord]) {
        $PSCmdlet.WriteError($_)
    } Else {
        $PSCmdlet.WriteError((New-Object System.Management.Automation.ErrorRecord -ArgumentList $_))
    }
}

The next full blown code example is only for Discussion purposes

Function Get-FunctionTemplate {

    [CmdletBinding()]
    Param(
        [Parameter()]
        [Object[]]$InputObject
    )

    
    Begin {
       
       $ErrorActionPreference = 'Stop'

        Try {

            # Code here
             # Code here
              # Code here
               # ...

        } Catch {

            $PSCmdlet.WriteError($_)

        } # Finally {}
    }
    
    Process {
         $ErrorActionPreference = 'Stop'

        ForEach ($Item in $InputObject) {
            
            Try {
                # Example to call a cmdlet with paranoic (double) Stop on Error
                Get-Item -Path 'C:\nosuch' -ErrorAction Stop
                
                # Code here
                 # Code here
                  # Code here
                   # ...
            
            
            } Catch {

                $PSCmdlet.WriteError($_)
            
            } # Finally {}
        }
    }

   End {

        Try {

            # Code here
             # Code here
              # Code here
               # ...

        } Catch {

            # Retrow the Error with different commands

            $PSCmdlet.WriteError($Error[0])
            # or
            Write-Error $Error[0]
            # or
            $PSCmdlet.WriteError($_)
            #or
            Write-Error $_

            #or
            Throw $Error[0]
            #or 
            Throw # $_

        } # Finally {}
    }
}

Kriegel avatar Oct 01 '19 13:10 Kriegel

I came here on my research of PowerShell event Logging (especially Log to text file) Logging and PowerShell is a very sad story ...

@Kriegel Welcome to PowerShell Core repo https://github.com/PowerShell/PowerShell to share and discuss your experience, proposals and thoughts.

iSazonov avatar Oct 01 '19 14:10 iSazonov

@iSazonov I do not understand why you pointing me to the PowerShell Repo. I am fully aware that I am here in the Docs section and the topic is

about_Error_Handling

Was my post not in that topic?

Kriegel avatar Oct 01 '19 16:10 Kriegel

@Kriegel My welcome is not related to the topic. I see you have great experience and we would appreciate your feedback in PowerShell repo too.

iSazonov avatar Oct 01 '19 16:10 iSazonov

@iSazonov O now i see! Thank you very much, for the welcome and your kind words to me! Back to topic... :-)

Kriegel avatar Oct 02 '19 06:10 Kriegel

I found 2 Documents that can help us with this topic. We can link or reuse the content of those writings:

PowerShell Practice and Style, Best-Practices, Error-Handling https://github.com/PoshCode/PowerShellPracticeAndStyle/blob/master/Best-Practices/Error-Handling.md

An Introduction to Error Handling in PowerShell https://gist.github.com/TravisEz13/9bb811c63b88501f3beec803040a9996

Kriegel avatar Oct 02 '19 12:10 Kriegel

@Kriegel: Glad to hear you found the OP useful, and thanks for your input.

Just a few quick responses :

inside directly after the Try { put the Statement $ErrorActionPreference = 'Stop' to make this work in the Try scope

The try compound statement actually does not create a new scope, so whatever you assign to $ErrorActionPreference in the try block remains in effect after the try statement.

inside the Catch {} use Write-Error $Error[0] to re-throw the Error.

Generally, Write-Error only respects the caller's preference if the caller is in the same scope domain as the callee - the problem discussed in https://github.com/PowerShell/PowerShell/issues/4568

Specifically, your try block $ErrorActionPreference = 'Stop' statement effectively makes the Write-Error always a script-terminating error.

i heard rumor that $_ can have an unexpected Value inside a Catch block.

I'm not personally aware of such cases, but do tell us if you know specifics.

Using $PSCmdlet.WriteError($) ? Really?

I agree that you shouldn't have to use $PSCmdlet.WriteError(), but the problem with Write-Error is that it doesn't set $? to $false in the caller's scope.

Then again, the usefulness of $? is currently severely compromised anyway - see https://github.com/PowerShell/PowerShell/issues/3359, which is now starting to become more of an issue in the RFC for implementing Bash-style && and || operators.

mklement0 avatar Oct 05 '19 21:10 mklement0

@mklement0 thank you for your response. Because I had read ALL cross references out of this discussion thread I am aware of most of your facts inside your response.

In one Point you I think you did not catch my intention. I mean $PSCmdlet.WriteError() is a bad candidate to rethrow, because it has no overload that accepts an .NET System.Exception Type. See #10735

Kriegel avatar Oct 08 '19 18:10 Kriegel

@Kriegel

I am aware of most of your facts inside your response.

I see. That wasn't obvious to me, since in your original comment you hadn't indicated awareness of operating based on mistaken assumptions, such as thinking that a try block creates a new scope, thinking that an $ErrorActionPreference = ... statement in there would be confined to it.

Perhaps you can edit a warning into your original comment.


As for $PSCmdlet.WriteError() / https://github.com/PowerShell/PowerShell/issues/10735: I think @vexx32 is correct there:

In a catch block - as far as I know - $_ is always a [System.Management.Automation.ErrorRecord] instance that you can directly pass to Write-Error / $PSCmdlet.WriteError().

However, note that with Write-Error you shouldn't just use Write-Error $_, because that will bind the error record to the -Message parameter (at least currently, unfortunately - see https://github.com/PowerShell/PowerShell/issues/10739), which converts it to a string that preserves the original exception's message only, wrapped in a [Microsoft.PowerShell.Commands.WriteErrorException] exception.

Instead, use Write-Error -ErrorRecord $_ (or $PSCmdlet.WriteError($__) if setting $? to $false in the caller's scope matters to you).


P.S.: Should you ever find yourself having to write an exception directly to the error stream, Write-Error actually does support that, via the -Exception parameter (e.g.: Write-Error -Exception ([System.InvalidOperationException]::new()) ...

mklement0 avatar Oct 08 '19 19:10 mklement0