Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is an exception from a .NET method a terminating or not-terminating error?

In this great article Keith explains the difference between terminating and non-terminating errors in Powershell. According to Keith exceptions thrown from calls to a member of a .NET object or type are non-terminating errors.

Indeed if we define this .NET class for testing:

$a = Add-Type 'public class bla { public static void bl() { throw new System.ApplicationException("test"); }}' -PassThru

And then this function:

function tst { 1 | write-host; $a::bl(); 2 | Write-host }

we will see that when tst function is called the exception appears to be non-terminating: the second Write-Host works.

But consider this:

function tst2 { try { tst } catch { "Catch!" } }

If we open the documentation, we can read that the catch responds to or handles terminating errors in scripts. Throughout the text of the articles the errors the article is dealing with qualified as "terminating" in numerous places.

So when we run the line above the second Write-Host does NOT run but the catch block does. It seems that our non-terminating error suddenly becomes terminating.

How come?

Another observation, is that with good old trap it's still non-terminating error:

function tst3 { tst trap { "Trap!" } }

Now, from practical perspective, what I want to achieve is the following. In a block of code I want to terminate on exceptions thrown from the .NET code. I want to leave terminating errors from cmdlets terminating and non-terminating errors from cmdlets non-terminating.

How do I achieve this?

Example:

Do-Something
Call::Something()
Do-SomethingElse
Call::SomethingElse()
Do-YetMoreSomething
Call::YetMoreSomething()

I want to terminate on all the exceptions from .NET calls above. I also want to terminate on terminating errors from the cmdlets. I do not want to terminate on non-terminating errors from the cmdlets.

like image 341
Andrew Savinykh Avatar asked Jul 25 '13 01:07

Andrew Savinykh


People also ask

What is .NET exception?

In . NET, an exception is an object that inherits from the System. Exception class. An exception is thrown from an area of code where a problem has occurred. The exception is passed up the stack until the application handles it or the program terminates.

What happens when an exception is thrown C#?

The Anatomy of C# Exceptions If any code throws an exception within that try block, the exception will be handled by the corresponding catch. catch – When an exception occurs, the Catch block of code is executed. This is where you are able to handle the exception, log it, or ignore it.

What are non-terminating errors in PowerShell?

Non-Terminating Error − This error is generally generated by internal cmdlets and automatically handled by them only but the error doesn't terminate the execution of the pipeline.

How do you handle exceptions in C#?

Use a try block around the statements that might throw exceptions. Once an exception occurs in the try block, the flow of control jumps to the first associated exception handler that is present anywhere in the call stack. In C#, the catch keyword is used to define an exception handler.


2 Answers

To complement Keith Hill's helpful answer:

PowerShell's error handling is complex, and matters are made worse by the fact that the documentation conflates two types of terminating errors:

  • Statement-terminating errors:

    • By default, they abort the current statement only and continue execution (with the next statement).
  • Script-terminating errors:

    • By default, they are fatal: they abort the entire script (more accurately, the entire call stack); the only way to generate them directly in PowerShell is with the Throw statement.

As an aside: The third type of error are non-terminating errors, whose primary purpose is to allow pipeline-processing cmdlets to report errors with respect to specific input objects, where such errors do not categorically prevent potentially successful processing of further input objects.


Unhandled .NET exceptions are statement-terminating errors, just like statement-terminating errors reported by cmdlets.

As such, you cannot fundamentally handle them differently using try / catch (which acts on both statement-terminating and script-terminating errors, but non non-terminating ones):

While you can test the [ErrorRecord] instance available as $_ in the catch block for its .Exception property being of type [System.Management.Automation.MethodInvocationExeption]
($_.Exception -is [System.Management.Automation.MethodInvocationExeption]),
it is too late to resume the default behavior for cmdlet-originated statement-terminating errors, because the try block has already been exited.


Treating all statement-terminating errors as fatal:

I want to terminate on all the exceptions from .NET calls above. I also want to terminate on terminating errors from the cmdlets. I do not want to terminate on non-terminating errors from the cmdlets.

Use the following in your script:

# Escalate any statement-terminating error to a script-terminating one,
# but leave non-terminating ones alone.
# (Already script-terminating errors still terminate the script.)
trap { break }

trap, like try / catch, acts on statement-terminating and script-terminating errors (but not non-terminating ones).

Use of break is crucial in order to terminate the script; by default, or when you use continue, execution continues with the next statement after the error-causing one, which even applies to script-terminating errors; continue additionally suppresses output of the error record (though it is still collected in the automatic $Error collection).


Treating all errors as fatal:

Note: Failures signaled by external programs via their exit code, reflected in automatic variable $LASTEXITCODE, are never considered errors by PowerShell (however, an opt-in mechanism to change that is being discussed; also, there are edge cases when an external program's stderr output indirectly triggers PowerShell errors - see this GitHub issue).

Setting:

 # !! Covers many, but NOT ALL scenarios - see below.
 $ErrorActionPreference = 'Stop'

is almost sufficient to make all error types fatal-by-default (script-terminating), but, sadly, not in all situations.

Note: Documented behavior claims that the $ErrorActionPreference preference variable only acts on non-terminating errors, but in reality it acts on statement-terminating ones as well; by contrast, the -ErrorAction common parameter indeed only acts on non-terminating errors.
As such, you do not strictly need to combine the preference variable with trap { break }.

Hypothetically, the above therefore makes all PowerShell errors fatal by default while allowing you to selectively override the behavior:

  • for non-terminating errors: on a per-command basis with common parameter -ErrorAction Continue/SilentlyContinue/Ignore (again, note that this does not act on statement/script-terminating errors)
  • for statement/script-terminating errors: with a command-enclosing try / catch

In practice, however, the approach fails for commands that happen to be implemented as advanced functions in a script module, which includes generated script modules that wrap remote cmdlets calls for implicit remoting:

Because of how variable scopes work in script modules, functions located in (different) script modules do NOT see the caller's preference variables (compiled cmdlets do not have this problem): see this GitHub issue.

In other words:

  • Functions from a different module will effectively ignore the caller's $ErrorActionPreference = 'Stop' value.
  • You may not even see it coming, given that it's not obvious which commands are implemented as binary cmdlets and which as functions.
  • Even if you do know which commands require a workaround, that workaround is cumbersome - see this answer of mine.

This GitHub docs issue tries to give a comprehensive overview of PowerShell's error handling and its pitfalls.

like image 77
mklement0 Avatar answered Sep 24 '22 07:09

mklement0


Yeah, this came up on the PowerShell MVP email list earlier this year. PowerShell changes its error handling behavior for .NET exceptions depending on whether or not there is an outer try/catch. This is just speculation but I'm guessing that this was for simple scripting scenarios. That is, if the scripter (admin) messes up calling a .NET method and that generates an exception, the PowerShell team didn't want that to cease execution of the entire script. Once V2 came along and introduced proper try/catch I'm guessing they had to revisit that decision and came up with the current compromise.

That said, working around this is a pain as you've discovered. You could set $ErrorActionPreference to Stop at the script level and then for every cmdlet that can generate non-terminating errors use the -ErrorAction Continue parameter. Or you could put all of your .NET invocations within an advanced function(s) and then call that function(s) with the parameter -ErrorAction Stop. I wish there was a better answer but after reviewing that MVP thread, I didn't see any better solutions.

like image 43
Keith Hill Avatar answered Sep 21 '22 07:09

Keith Hill