15

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.

3
  • 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. Commented Jun 21, 2011 at 19:42
  • 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. Commented Jun 22, 2011 at 13:49
  • 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. Commented Jun 22, 2011 at 13:53

6 Answers 6

20

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.

Sign up to request clarification or add additional context in comments.

1 Comment

I couldn't get @alek solution to work. Setting -ErrorAction:SilentlyContinue just ignores the error. Could this be because MS have changed something in PS 5.1?
8

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.

2 Comments

With the continue there, shouldn't there be a line saying ` end 2` in between the exception info and the line with begin 3? If not, why not?
@CodeJockey The trap is in the script, not in the function, so the continue is executing in the outer script after the function has exited. If you move the trap block into the function, then you will get the behavior you describe.
5

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.

3 Comments

(Please ignore last comment) I'd like to avoid throwing exceptions and try/catch blocks if possible. Ideally the functions should act just like cmdlets - add ErrorRecords to the error stream that can be handled/filtered/ignored/etc. in many different ways, however the caller decides. Throwing exceptions seems harsh as it terminates the current function and script execution unless explicitly caught. I prefer the simple elegance of $?...
Cmdlets throw exceptions, which end up in the error stream. So throwing exceptions would be acting just like Cmdlets. Exceptions are the proper way to signal a failed command.
If you are going to rethrow an exception, use throw $_ so you don't eat the stack trace or other valuable information.
1

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

Comments

0

$? 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

2 Comments

-ErrorAction Stop and throw do work - they set $? to $false - but the $? is never displayed afterward in your example as the script terminates because of the -ErrorAction Stop or throw. If the foo call is wrapped in a try/catch it works but I was hoping to avoid having to wrap every function call in try/catch. If you use try/catch there's no point in checking $? because if you are in the catch block, an error occurred.
What I'd like to do is below - it's clean but handles all errors: foo -ErrorVariable Err if ($? -eq $false) { ReportError $Err } You can also pass the function name and params easily (if using splatting) to ReportError.
0

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...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.