12
votes

What is the best way to detect if an error occurs in a script function? I'm looking for a consistent way to indicate error/success status similar to $? (which only works on cmdlets, not script functions).

Given a particular function might return a value to be used by the caller, we can't indicate success by returning a boolean. A function could use a [ref] parameter and set the value appropriately inside the function and check after the call, but this is more overhead than I'd like. Is there something built-in to PowerShell that we can use?

The best I can come up with is:

  1. In the function use Write-Error to put ErrorRecord objects in the error stream;
  2. call the function with an ErrorVariable parameter;
  3. check if the ErrorVariable parameter is not null after the call.

For example:

function MyFun {
  [CmdletBinding()]    # must be an advanced function or this 
  param ()             # will not work as ErrorVariable will not exist
  process {
    # code here....
    if ($SomeErrorCondition) {
      Write-Error -Message "Error occurred; details..."
      return
    }
    # more code here....
  }
}

# call the function
$Err = $null
MyFun -ErrorVariable Err
# this check would be similar to checking if $? -eq $false
if ($Err -ne $null) {
  "An error was detected"
  # handle error, log/email contents of $Err, etc.
}

Is there something better? Is there a way of using $? in our script functions? I'd rather not throw exceptions or ErrorRecord objects and have tons of try/catch blocks all over the place. I'd also rather not use $Error as would require checking the count before making the function call as there could be other errors in there before the call - and I don't want to Clear() and lose them.

6
Why don't you want to use try/catch? It doesn't add much more than the if statement does; try{}catch{} vs. if($err){} is only a 2 char difference.Rynant
What I'd like to do is: foo -ev Err if ($? -eq $false) { ReportError $Err } Wrapping every function call in try/catch is distracting to me; following every function call with a if (...) { ReportError ... } seems cleaner. I would prefer to only wrap code in try/catch if I really can't catch/prevent a terminating error otherwise.DanW
Think of dir nodrivefound:\ ErrorRecords are put in the error stream and can be captured with 2> or -ErrorVariable, $? is $false so the error is easily detected but the script doesn't terminate and there's no try/catch. I was hoping for something like that.DanW

6 Answers

19
votes

What is the best way to detect if an error occurs in a script function? I'm looking for a consistent way to indicate error/success status similar to $? (which only works on cmdlets, not script functions).

Error handling in PowerShell is a total mess. There are error records, script exceptions, .NET exceptions, $?, $LASTEXITCODE, traps, $Error array (between scopes), and so on. And constructs to interact these elements with each other (such as $ErrorActionPreference). It is very difficult to get consistent when you have a morass like this; however, there is a way to achieve this goal.

The following observations must be made:

  • $? is an underdocumented mystery. $? values from cmdlet calls do not propagate, it is a "read-only variable" (thus cannot be set by hand) and it is not clear on when exactly it gets set (what could possibly be an "execution status", term never used in PowerShell except on the description of $? in about_Automatic_Variables, is an enigma). Thankfully Bruce Payette has shed light on this: if you want to set $?, $PSCmdlet.WriteError() is the only known way.

  • If you want functions to set $? as cmdlets do, you must refrain from Write-Error and use $PSCmdlet.WriteError() instead. Write-Error and $PSCmdlet.WriteError() do the same thing, but the former does not set $? properly and the latter does. (Do not bother trying to find this documented somewhere. It is not.)

  • If you want to handle .NET exceptions properly (as if they were non-terminating errors, leaving the decision of halting the entire execution up to the client code), you must catch and $PSCmdlet.WriteError() them. You cannot leave them unprocessed, since they become non-terminating errors which do not respect $ErrorActionPreference. (Not documented either.)

In other words, the key to produce consistent error handling behavior is to use $PSCmdlet.WriteError() whenever possible. It sets $?, respects $ErrorActionPreference (and thus -ErrorAction) and accepts System.Management.Automation.ErrorRecord objects produced from other cmdlets or a catch statement (in the $_ variable).

The following examples will show how to use this method.

# Function which propagates an error from an internal cmdlet call,
# setting $? in the process.
function F1 {
    [CmdletBinding()]
    param([String[]]$Path)

    # Run some cmdlet that might fail, quieting any error output.
    Convert-Path -Path:$Path -ErrorAction:SilentlyContinue
    if (-not $?) {
        # Re-issue the last error in case of failure. This sets $?.
        # Note that the Global scope must be explicitly selected if the function is inside
        # a module. Selecting it otherwise also does not hurt.
        $PSCmdlet.WriteError($Global:Error[0])
        return
    }

    # Additional processing.
    # ...
}


# Function which converts a .NET exception in a non-terminating error,
# respecting both $? and $ErrorPreference.
function F2 {
    [CmdletBinding()]
    param()

    try {
        [DateTime]"" # Throws a RuntimeException.
    }
    catch {
        # Write out the error record produced from the .NET exception.
        $PSCmdlet.WriteError($_)
        return
    }
}

# Function which issues an arbitrary error.
function F3 {
    [CmdletBinding()]
    param()

    # Creates a new error record and writes it out.
    $PSCmdlet.WriteError((New-Object -TypeName:"Management.Automation.ErrorRecord"
        -ArgumentList:@(
            [Exception]"Some error happened",
            $null,
            [Management.Automation.ErrorCategory]::NotSpecified,
            $null
        )
    ))

    # The cmdlet error propagation technique using Write-Error also works.
    Write-Error -Message:"Some error happened" -Category:NotSpecified -ErrorAction:SilentlyContinue
    $PSCmdlet.WriteError($Global:Error[0])
}

As a last note, if you want to create terminating errors from .NET exceptions, do try/catch and rethrow the exception caught.

8
votes

It sounds like you are looking for a general mechanism to log any error occurring in a command called from a script. If so, trap is probably the most appropriate mechanism:

Set-Alias ReportError Write-Host -Scope script  # placeholder for actual logging

trap {
  ReportError @"
Error in script $($_.InvocationInfo.ScriptName) :
$($_.Exception) $($_.InvocationInfo.PositionMessage)
"@
  continue  # or use 'break' to stop script execution
}

function f( [int]$a, [switch]$err ) {
  "begin $a"
  if( $err ) { throw 'err' }
  "  end $a"
}

f 1
f 2 -err
f 3

Running this test script produces the following output, without requiring any modification to the called functions:

PS> ./test.ps1
begin 1
  end 1
begin 2
Error in script C:\Users\umami\t.ps1 :
System.Management.Automation.RuntimeException: err
At C:\Users\umami\t.ps1:13 char:21
+   if( $err ) { throw <<<<  'err' }
begin 3
  end 3

If script execution should stop after an error is reported, replace continue with break in the trap handler.

4
votes

Two things come into mind: Throw (better than Write-Error in your example above), and try..catch

try
{
   #code here
}
catch
{
   if ($error[0].Exception -match "some particular error")
   {
       Write-Error "Oh No! You did it!"
   }
   else
   {
       Throw ("Ooops! " + $error[0].Exception)
   }
}

Imho, it is generally better to have the function itself to handle its errors as much as possible.

1
votes

I believe you want a global variable $GLOBAL:variable_name. That variable will be in the scope of the script not just the function.

Looking at the code you may want to use trap (Get-Help about_Trap) as well - though $GLOBAL:variable_name would work with yours above. Here's a re-wreite of the code example - I've not tested this so it's more pseudo-code... :)

function MyFun {
  [CmdletBinding()]    # must be an advanced function or this 
  param ()             # will not work as ErrorVariable will not exist
  begin {
    trap {
      $GLOBAL:my_error_boolean = $true
      $GLOBAL:my_error_message = "Error Message?"

      return
    }
  }
  process {
    # more code here....
  }
}

# call the function
$GLOBAL:my_error_boolean = $false
MyFun 
# this check would be similar to checking if $? -eq $false
if ($GLOBAL:my_error_boolean) {
  "An error was detected"
  # handle error, log/email contents of $Err, etc.
}

HTH, Matt

0
votes

$? depends on if the function throw a terminating error or not. If Write-Error is used, not Throw, $? is not set. Many cmdlets don't set $? when they have an error, because that error is not a terminating error.

The easiest way to make your function set $? is to use -ErrorAction Stop. This will stop the script when your function errors, and $? will be set.

Note this block of samples to see how $? works:

function foo([ParameteR()]$p) { Write-Error "problem" } 

foo 

$?

foo -errorAction Stop



$?

function foo() { throw "problem" } 

foo 

$?

Hope this helps

0
votes

Most of this made a lovely whooshing sound as it went right over my head... ಠ_ಠ

I am with Dan. PS Logging is a complete mess and seems like it will more than double the size of the code I am writing...

Frankly, I would be happy if I could just capture console output directly to logs, warts and all...

The Try/Catch block is so ... so ... crappy, I can smell it and it has made my eyes turn brown.

The $? is very interesting, but you guys actually know what you are doing, as where I am at the point where I have realized I know nothing (last week I thought I knew at least something, but noooooo).

Why the %$#@%$ isn't there something like the 2> in cli ...

Ok so here is what I am trying to do (you've read this far, so why not?):

    Function MyFunc($Param1, $Param2){
Do{
  $Var = Get-Something | Select Name, MachineName, Status 
 $NotherVar = Read-Host -Prompt "Do you want to Stop or Start or check the $Var (1 to Start, 2 to stop, 3 to check, 4 to continue)?" 
    If ($SetState -eq 1) 
     {
      Do Stuff
    }
    ElseIf ($Var -eq 2) 
       {
      Do Stuff
    }
    ElseIf ($Var -eq 3)
       {
      Do Stuff
    }
  }
    Until ($Var -eq 4)
Do other stuff
} 

Did it work? Yes, fine... Log it and continue. No? Then catch the error, log it and continue the script...

I am tempted to just ask for user input, add-content and continue...

Incidentally, I did find a module PSLogging that seems like it would be pretty cool, but I am not sure how to get it working... The documentation is a bit Spartan. Seems like folks are getting it working without too much trouble, so I kinda feel like I am a corner sitting pointy hat kind of person...