Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PowerShell: detecting errors in script functions

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.

like image 978
DanW Avatar asked Jun 21 '11 18:06

DanW


People also ask

How do I check for errors in PowerShell script?

Use the try block to define a section of a script in which you want PowerShell to monitor for errors. When an error occurs within the try block, the error is first saved to the $Error automatic variable. PowerShell then searches for a catch block to handle the error.

Which cmdlet can be used to find error in the script?

The Get-Error cmdlet gets a PSExtendedError object that represents the current error details from the last error that occurred in the session. You can use Get-Error to display a specified number of errors that have occurred in the current session using the Newest parameter.

What does %% mean in PowerShell?

% is an alias for the ForEach-Object cmdlet. An alias is just another name by which you can reference a cmdlet or function.

What does SilentlyContinue do in PowerShell?

SilentlyContinue — Don't display an error message continue to execute subsequent commands. Continue — Display any error message and attempt to continue execution of subsequence commands. Inquire — Prompts the user whether to continue or terminate the action. Stop — Terminate the action with error.


2 Answers

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.

like image 53
alecov Avatar answered Sep 21 '22 05:09

alecov


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.

like image 45
Emperor XLII Avatar answered Sep 21 '22 05:09

Emperor XLII