Can I get detailed exception stacktrace in PowerShell?

ExceptionPowershellStack Trace

Exception Problem Overview


Runing such script:

 1: function foo()
 2: {
 3:    bar
 4: }
 5: 
 6: function bar()
 7: {
 8:     throw "test"
 9: }
10: 
11: foo

I see

test
At C:\test.ps1:8 char:10

Can I get a detailed stack trace instead?

At bar() in C:\test.ps1:8
At foo() in C:\test.ps1:3 
At C:\test.ps1:11

Exception Solutions


Solution 1 - Exception

There is a function up on the PowerShell Team blog called Resolve-Error which will get you all kinds of details

Note that $error is an array of all the errors you have encountered in your PSSession. This function will give you details on the last error you encountered.

function Resolve-Error ($ErrorRecord=$Error[0])
{
   $ErrorRecord | Format-List * -Force
   $ErrorRecord.InvocationInfo |Format-List *
   $Exception = $ErrorRecord.Exception
   for ($i = 0; $Exception; $i++, ($Exception = $Exception.InnerException))
   {   "$i" * 80
       $Exception |Format-List * -Force
   }
}

Solution 2 - Exception

Powershell 3.0 adds a ScriptStackTrace property to the ErrorRecord object. I use this function for error reporting:

function Write-Callstack([System.Management.Automation.ErrorRecord]$ErrorRecord=$null, [int]$Skip=1)
{
    Write-Host # blank line
    if ($ErrorRecord)
    {
        Write-Host -ForegroundColor Red "$ErrorRecord $($ErrorRecord.InvocationInfo.PositionMessage)"
	
        if ($ErrorRecord.Exception)
        {
            Write-Host -ForegroundColor Red $ErrorRecord.Exception
        }
	
        if ((Get-Member -InputObject $ErrorRecord -Name ScriptStackTrace) -ne $null)
        {
            #PS 3.0 has a stack trace on the ErrorRecord; if we have it, use it & skip the manual stack trace below
            Write-Host -ForegroundColor Red $ErrorRecord.ScriptStackTrace
            return
        }
    }

    Get-PSCallStack | Select -Skip $Skip | % {
        Write-Host -ForegroundColor Yellow -NoNewLine "! "
        Write-Host -ForegroundColor Red $_.Command $_.Location $(if ($_.Arguments.Length -le 80) { $_.Arguments })
    }
}

The Skip parameter lets me leave Write-Callstack or any number of error-handling stack frames out of the Get-PSCallstack listing.

Note that if called from a catch block, Get-PSCallstack will miss any frames between the throw site and the catch block. Hence I prefer the PS 3.0 method even though we have fewer details per frame.

Solution 3 - Exception

There is the automatic variable $StackTrace but it seems to be a little more specific to internal PS details than actually caring about your script, so that won't be of much help.

There is also Get-PSCallStack but that's gone as soon as you hit the exception, unfortunately. You could, however, put a Get-PSCallStack before every throw in your script. That way you get a stack trace immediately before hitting an exception.

I think one could script such functionality by using the debugging and tracing features of Powershell but I doubt it'd be easy.

Solution 4 - Exception

You can not get a stack trace from exceptions of the PowerShell code of scripts, only from .NET objects. To do that, you will need to get the Exception object like one of these:

$Error[0].Exception.StackTrace
$Error[0].Exception.InnerException.StackTrace
$Error[0].StackTrace

Solution 5 - Exception

I took what I found here as inspiration and created a nice function anyone can drop into their code.

This is how I call it: Write-Host "Failed to write to the log file `n$(Resolve-Error)" -ForegroundColor Red

Function Resolve-Error
{
<#
.SYNOPSIS
    Enumerate error record details.
    
.DESCRIPTION
    Enumerate an error record, or a collection of error record, properties. By default, the details
    for the last error will be enumerated.
    
.PARAMETER ErrorRecord
    The error record to resolve. The default error record is the lastest one: $global:Error[0].
    This parameter will also accept an array of error records.
    
.PARAMETER Property
    The list of properties to display from the error record. Use "*" to display all properties.
    Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException
    
    Below is a list of all of the possible available properties on the error record:
    
    Error Record:               Error Invocation:           Error Exception:                    Error Inner Exception(s):
    $_                          $_.InvocationInfo           $_.Exception                        $_.Exception.InnerException
    -------------               -----------------           ----------------                    ---------------------------
    writeErrorStream            MyCommand                   ErrorRecord                         Data
    PSMessageDetails            BoundParameters             ItemName                            HelpLink
    Exception                   UnboundArguments            SessionStateCategory                HResult
    TargetObject                ScriptLineNumber            StackTrace                          InnerException
    CategoryInfo                OffsetInLine                WasThrownFromThrowStatement         Message
    FullyQualifiedErrorId       HistoryId                   Message                             Source
    ErrorDetails                ScriptName                  Data                                StackTrace
    InvocationInfo              Line                        InnerException                      TargetSite
    ScriptStackTrace            PositionMessage             TargetSite                          
    PipelineIterationInfo       PSScriptRoot                HelpLink                            
                                PSCommandPath               Source                              
                                InvocationName              HResult                             
                                PipelineLength              
                                PipelinePosition            
                                ExpectingInput              
                                CommandOrigin               
                                DisplayScriptPosition       
    
.PARAMETER GetErrorRecord
    Get error record details as represented by $_
    Default is to display details. To skip details, specify -GetErrorRecord:$false
    
.PARAMETER GetErrorInvocation
    Get error record invocation information as represented by $_.InvocationInfo
    Default is to display details. To skip details, specify -GetErrorInvocation:$false
    
.PARAMETER GetErrorException
    Get error record exception details as represented by $_.Exception
    Default is to display details. To skip details, specify -GetErrorException:$false
    
.PARAMETER GetErrorInnerException
    Get error record inner exception details as represented by $_.Exception.InnerException.
    Will retrieve all inner exceptions if there is more then one.
    Default is to display details. To skip details, specify -GetErrorInnerException:$false
    
.EXAMPLE
    Resolve-Error
    
    Get the default error details for the last error
    
.EXAMPLE
    Resolve-Error -ErrorRecord $global:Error[0,1]
    
    Get the default error details for the last two errors

.EXAMPLE
    Resolve-Error -Property *
    
    Get all of the error details for the last error
    
.EXAMPLE
    Resolve-Error -Property InnerException
    
    Get the "InnerException" for the last error
    
.EXAMPLE
    Resolve-Error -GetErrorInvocation:$false
    
    Get the default error details for the last error but exclude the error invocation information
    
.NOTES
.LINK
#>
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullorEmpty()]
        [array]$ErrorRecord,
        
        [Parameter(Mandatory=$false, Position=1)]
        [ValidateNotNullorEmpty()]
        [string[]]$Property = ('Message','InnerException','FullyQualifiedErrorId','ScriptStackTrace','PositionMessage'),
        
        [Parameter(Mandatory=$false, Position=2)]
        [switch]$GetErrorRecord = $true,
        
        [Parameter(Mandatory=$false, Position=3)]
        [switch]$GetErrorInvocation = $true,
        
        [Parameter(Mandatory=$false, Position=4)]
        [switch]$GetErrorException = $true,
        
        [Parameter(Mandatory=$false, Position=5)]
        [switch]$GetErrorInnerException = $true
    )
    
    Begin
    {
        ## If function was called without specifying an error record, then choose the latest error that occured
        If (-not $ErrorRecord)
        {
            If ($global:Error.Count -eq 0)
            {
                # The `$Error collection is empty
                Return
            }
            Else
            {
                [array]$ErrorRecord = $global:Error[0]
            }
        }
        
        ## Define script block for selecting and filtering the properties on the error object
        [scriptblock]$SelectProperty = {
            Param
            (
                [Parameter(Mandatory=$true)]
                [ValidateNotNullorEmpty()]
                $InputObject,
                
                [Parameter(Mandatory=$true)]
                [ValidateNotNullorEmpty()]
                [string[]]$Property
            )
            [string[]]$ObjectProperty = $InputObject | Get-Member -MemberType *Property | Select-Object -ExpandProperty Name
            ForEach ($Prop in $Property)
            {
                If ($Prop -eq '*')
                {
                    [string[]]$PropertySelection = $ObjectProperty
                    Break
                }
                ElseIf ($ObjectProperty -contains $Prop)
                {
                    [string[]]$PropertySelection += $Prop
                }
            }
            Write-Output $PropertySelection
        }
        
        # Initialize variables to avoid error if 'Set-StrictMode' is set
        $LogErrorRecordMsg      = $null
        $LogErrorInvocationMsg  = $null
        $LogErrorExceptionMsg   = $null
        $LogErrorMessageTmp     = $null
        $LogInnerMessage        = $null
    }
    Process
    {
        ForEach ($ErrRecord in $ErrorRecord)
        {
            ## Capture Error Record
            If ($GetErrorRecord)
            {
                [string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrRecord -Property $Property
                $LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties
            }
            
            ## Error Invocation Information
            If ($GetErrorInvocation)
            {
                If ($ErrRecord.InvocationInfo)
                {
                    [string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property
                    $LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties
                }
            }
            
            ## Capture Error Exception
            If ($GetErrorException)
            {
                If ($ErrRecord.Exception)
                {
                    [string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrRecord.Exception -Property $Property
                    $LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties
                }
            }
            
            ## Display properties in the correct order
            If ($Property -eq '*')
            {
                # If all properties were chosen for display, then arrange them in the order
                #  the error object displays them by default.
                If ($LogErrorRecordMsg)     {[array]$LogErrorMessageTmp += $LogErrorRecordMsg    }
                If ($LogErrorInvocationMsg) {[array]$LogErrorMessageTmp += $LogErrorInvocationMsg}
                If ($LogErrorExceptionMsg)  {[array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
            }
            Else
            {
                # Display selected properties in our custom order
                If ($LogErrorExceptionMsg)  {[array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
                If ($LogErrorRecordMsg)     {[array]$LogErrorMessageTmp += $LogErrorRecordMsg    }
                If ($LogErrorInvocationMsg) {[array]$LogErrorMessageTmp += $LogErrorInvocationMsg}
            }
            
            If ($LogErrorMessageTmp)
            {
                $LogErrorMessage  = 'Error Record:'
                $LogErrorMessage += "`n-------------"
                $LogErrorMsg      = $LogErrorMessageTmp | Format-List | Out-String
                $LogErrorMessage += $LogErrorMsg
            }
            
            ## Capture Error Inner Exception(s)
            If ($GetErrorInnerException)
            {
                If ($ErrRecord.Exception -and $ErrRecord.Exception.InnerException)
                {
                    $LogInnerMessage  = 'Error Inner Exception(s):'
                    $LogInnerMessage += "`n-------------------------"
                    
                    $ErrorInnerException = $ErrRecord.Exception.InnerException
                    $Count = 0
                    
                    While ($ErrorInnerException)
                    {
                        $InnerExceptionSeperator = '~' * 40
                        
                        [string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrorInnerException -Property $Property
                        $LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String
                        
                        If ($Count -gt 0)
                        {
                            $LogInnerMessage += $InnerExceptionSeperator
                        }
                        $LogInnerMessage += $LogErrorInnerExceptionMsg
                        
                        $Count++
                        $ErrorInnerException = $ErrorInnerException.InnerException
                    }
                }
            }
            
            If ($LogErrorMessage) { $Output += $LogErrorMessage }
            If ($LogInnerMessage) { $Output += $LogInnerMessage }
            
            Write-Output $Output
            
            If (Test-Path -Path 'variable:Output'            ) { Clear-Variable -Name Output             }
            If (Test-Path -Path 'variable:LogErrorMessage'   ) { Clear-Variable -Name LogErrorMessage    }
            If (Test-Path -Path 'variable:LogInnerMessage'   ) { Clear-Variable -Name LogInnerMessage    }
            If (Test-Path -Path 'variable:LogErrorMessageTmp') { Clear-Variable -Name LogErrorMessageTmp }
        }
    }
    End {}
}

Solution 6 - Exception

This code:

try {
    ...
}
catch {
    Write-Host $_.Exception.Message -Foreground "Red"
    Write-Host $_.ScriptStackTrace -Foreground "DarkGray"
    exit 1
}

Will echo an error in a following format:

No match was found for the specified search criteria and module names 'psake'.
at Get-InstalledModule<Process>, ...\PSModule.psm1: line 9251
at Import-ModuleThirdparty, ...\Import-ModuleThirdparty.psm1: line 3
at <ScriptBlock>, ...\index.ps1: line 13

Solution 7 - Exception

Here's a way: Tracing the script stack

The core of it is this code:

1..100 | %{ $inv = &{ gv -sc $_ myinvocation }

Solution 8 - Exception

I just figured it out. The $_ is the exception caught in the catch block.

$errorString= $_ | Out-String 

Solution 9 - Exception

You can also change the default formatting for the error object to include the stack trace. Basically, make your format file by copying the chunk for System.Management.Automation.ErrorRecord from $PSHOME\PowerShellCore.format.ps1xml and add your own element that adds the trace. Then load it with Update-FormatData. For more details, I've just written a blog post about it: https://blogs.msdn.microsoft.com/sergey_babkins_blog/2016/12/28/getting-a-stack-trace-in-powershell/

Oh, one more thing: this doesn't propagate automatically into the remote sessions. The objects get formatted into strings on the remote side. For stack traces in the remote sessions you'll have to upload this file there and call Update-FormatData there again.

Solution 10 - Exception

Stumbled upon this looking for a built in solution. I am going with simple solution. Just add trace block before using any powershell. This will ensure a call stack is shown. Down side of this is the stack will be displayed before the error message.

Trace {
   $_.ScriptStackTrace
}

Solution 11 - Exception

Maybe I've misunderstood something, but my issue here is that I've not been seeing powershell script stack traces for inner exceptions.

In the end I ended up searching $Global:Error for an exception's Error Eecord object to retrieve the script stack trace.

<#
.SYNOPSIS
   Expands all inner exceptions and provides Script Stack Traces where available by mapping Exceptions to ErrorRecords

.NOTES
   Aggregate exceptions aren't full supported, and their child exceptions won't be traversed like regular inner exceptions
   
#>
function Get-InnerErrors ([Parameter(ValueFrompipeline)] $ErrorObject=$Global:Error[0])
{
   # Get the first exception object from the input error
   $ex = $null
   if( $ErrorObject -is [System.Management.Automation.ErrorRecord] ){
       $ex = $ErrorObject.Exception 
   }
   elseif( $ErrorObject -is [System.Exception] ){
       $ex = $ErrorObject
   } 
   else
   {
       throw "Unexpected error type for Get-InnerErrors: $($ErrorObject.GetType()):`n $ErrorObject"
   }

   Write-Debug "Walking inner exceptions from exception"
   for ($i = 0; $ex; $i++, ($ex = $ex.InnerException))
   {  
       $ErrorRecord = $null

       if( $ex -is [System.Management.Automation.IContainsErrorRecord] ){ 

           Write-Debug "Inner Exception $i : Skipping search for ErrorRecord in `$Global:Error, exception type implements IContainsErrorRecord" 
           $ErrorRecord = ([System.Management.Automation.IContainsErrorRecord]$ex).ErrorRecord
       }
       else {

           # Find ErrorRecord for exception by mapping exception to ErrorRecrods in $Global:Error

           ForEach( $err in $Global:Error ){# Need to use Global scope when referring from a module
               if( $err -is [System.Management.Automation.ErrorRecord] ){
                  if( $err.Exception -eq $ex ){
                       $ErrorRecord = $err 
                       Write-Debug "Inner Exception $i : Found ErrorRecord in `$Global:Error" 
                       break
                   }
               } 
               elseif( $err -is [System.Management.Automation.IContainsErrorRecord] ) {
                   if( $err -eq $ex -or $err.ErrorRecord.Exception -eq $ex ){
                       $ErrorRecord = $err.ErrorRecord
                       Write-Debug "Inner Exception $i : Found ErrorRecord in `$Global:Error" 
                       break
                   }
               } 
               else {
                   Write-Warning "Unexpected type in `$Global:Error[]. Expected `$Global:Error to always be an ErrorRecrod OR exception implementing IContainsErrorRecord but found type: $($err.GetType())"
               }
           }
       }

       if( -not($ErrorRecord) ){
           Write-Debug "Inner Exception $i : No ErrorRecord could be found for exception of type: $($ex.GetType())" 
       }
       
      
       # Return details as custom object

       [PSCustomObject] @{
           ExceptionDepth      = $i
           Message  	        = $ex.Message
           ScriptStackTrace	= $ErrorRecord.ScriptStackTrace  # Note ErrorRecord will be null if exception was not from Powershell
           ExceptionType    	= $ex.GetType().FullName
           ExceptionStackTrace	= $ex.StackTrace
       }
   }
}

Example Usage:

function Test-SqlConnection
{
    $ConnectionString = "ThisConnectionStringWillFail"
	try{
	    $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $ConnectionString;
		$sqlConnection.Open();
	}
	catch
	{
		throw [System.Exception]::new("Sql connection failed with connection string: '$ConnectionString'", $_.Exception)
	}
	finally
	{
        if($sqlConnection){
		    $sqlConnection.Close();
        }
	}
}


try{
    Test-SqlConnection
}
catch {
    Get-InnerErrors $_
}

Example output:



ExceptionDepth      : 0
Message             : Sql connection failed with connection string: 'ThisConnectionStringWillFail'
ScriptStackTrace    : at Test-SqlConnection, <No file>: line 11
                      at <ScriptBlock>, <No file>: line 23
ExceptionType       : System.Exception
ExceptionStackTrace : 

ExceptionDepth      : 1
Message             : Exception calling ".ctor" with "1" argument(s): "Format of the initialization string does not conform to specification starting at index 0."
ScriptStackTrace    : 
ExceptionType       : System.Management.Automation.MethodInvocationException
ExceptionStackTrace :    at System.Management.Automation.DotNetAdapter.AuxiliaryConstructorInvoke(MethodInformation methodInformation, Object[] arguments, Object[] originalArguments)
                         at System.Management.Automation.DotNetAdapter.ConstructorInvokeDotNet(Type type, ConstructorInfo[] constructors, Object[] arguments)
                         at Microsoft.PowerShell.Commands.NewObjectCommand.CallConstructor(Type type, ConstructorInfo[] constructors, Object[] args)

ExceptionDepth      : 2
Message             : Format of the initialization string does not conform to specification starting at index 0.
ScriptStackTrace    : 
ExceptionType       : System.ArgumentException
ExceptionStackTrace :    at System.Data.Common.DbConnectionOptions.GetKeyValuePair(String connectionString, Int32 currentPosition, StringBuilder buffer, Boolean useOdbcRules, String& keyname, String& 
                      keyvalue)
                         at System.Data.Common.DbConnectionOptions.ParseInternal(Hashtable parsetable, String connectionString, Boolean buildChain, Hashtable synonyms, Boolean firstKey)
                         at System.Data.Common.DbConnectionOptions..ctor(String connectionString, Hashtable synonyms, Boolean useOdbcRules)
                         at System.Data.SqlClient.SqlConnectionString..ctor(String connectionString)
                         at System.Data.SqlClient.SqlConnectionFactory.CreateConnectionOptions(String connectionString, DbConnectionOptions previous)
                         at System.Data.ProviderBase.DbConnectionFactory.GetConnectionPoolGroup(DbConnectionPoolKey key, DbConnectionPoolGroupOptions poolOptions, DbConnectionOptions& userConnectionOptions)
                         at System.Data.SqlClient.SqlConnection.ConnectionString_Set(DbConnectionPoolKey key)
                         at System.Data.SqlClient.SqlConnection.set_ConnectionString(String value)
                         at System.Data.SqlClient.SqlConnection..ctor(String connectionString, SqlCredential credential)

Solution 12 - Exception

There are cases where PowerShell doesn't seem to keep a backtrace, like calling a method or calling a function with .Invoke(). For that, Set-PSDebug -Trace 2 may come in handy. It will print every executed line of the running script.

Try flipping # on (1) and (2) and running WrapStackTraceLog({ function f{ 1/0 } ; & f }) # let's divide by zero

Function WrapStackTraceLog($func) {
    try {
        # return $func.Invoke($args)  # (1)
        return (& $func $args)  # (2)
    } catch {
        Write-Host ('=' * 70)
        Write-Host $_.Exception.Message
        Write-Host ('-' * 70)
        Write-Host $_.ScriptStackTrace
        Write-Host ('-' * 70)
        Write-Host "$StackTrace"
        Write-Host ('=' * 70)
    }
}

Branch (1) exception caught:

Exception calling "Invoke" with "1" argument(s): "Attempted to divide by zero."

Branch (2) is more informative:

at f, <No file>: line 1
at <ScriptBlock>, <No file>: line 1
at global:WrapStackTraceLog, <No file>: line 4
at <ScriptBlock>, <No file>: line 1

But, you can still trace your Invokes with tracing on, branch (1):

DEBUG:     ! CALL function 'f'
DEBUG:    1+ WrapStackTraceLog({ function f{  >>>> 1/0 } ; & f })
DEBUG:    6+          >>>> Write-Host ('=' * 70)
======================================================================
DEBUG:    7+          >>>> Write-Host $_.Exception.Message
Exception calling "Invoke" with "1" argument(s): "Attempted to divide by zero."

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionalex2k8View Question on Stackoverflow
Solution 1 - ExceptionAndy SchneiderView Answer on Stackoverflow
Solution 2 - ExceptionTim SparklesView Answer on Stackoverflow
Solution 3 - ExceptionJoeyView Answer on Stackoverflow
Solution 4 - ExceptionJasonMArcherView Answer on Stackoverflow
Solution 5 - Exceptionuser1367200View Answer on Stackoverflow
Solution 6 - ExceptionVitaliy UlantikovView Answer on Stackoverflow
Solution 7 - ExceptionJay BazuziView Answer on Stackoverflow
Solution 8 - ExceptionmjquitoView Answer on Stackoverflow
Solution 9 - ExceptionSergey BabkinView Answer on Stackoverflow
Solution 10 - ExceptionRichard MeadowsView Answer on Stackoverflow
Solution 11 - ExceptionJonoBView Answer on Stackoverflow
Solution 12 - ExceptionRabashView Answer on Stackoverflow