Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Throw an exception in powershell with nesting original error

Tags:

powershell

I'm a C# developer who is trying to build something useful using PowerShell. That's why I'm keep trying to use well-known idioms from .NET world in PowerShell.

I'm writing a script that has different layer of abstractions: database operations, file manipulation etc. At some point I would like to catch an error and wrap it into something more meaningful for the end user. This is a common pattern for C#/Java/C++ code:

Function LowLevelFunction($arg)
{
  # Doing something very useful here!
  # but this operation could throw
  if (!$arg) {throw "Ooops! Can't do this!"}
}

Now, I would like to call this function and wrap an error:

Function HighLevelFunction
{
  Try
  {
     LowLevelFunction
  }
  Catch
  {
     throw "HighLevelFunction failed with an error!`nPlease check inner exception for more details!`n$_"
  }
}

This approach is almost what I need, because HighLevelFunction will throw new error and the root cause of the original error would be lost!

In C# code I always can throw new exception and provide original exception as an inner exception. In this case HighLevelFunction would be able to communicate their errors in a form more meaningful for their clients but still will provide inner details for diagnostic purposes.

The only way to print original exception in PowerShell is to use $Error variable that stores all the exceptions. This is OK, but the user of my script (myself for now) should do more things that I would like.

So the question is: Is there any way to raise an exception in PowerShell and provide original error as an inner error?

like image 321
Sergey Teplyakov Avatar asked Jun 03 '15 16:06

Sergey Teplyakov


2 Answers

You can throw a new exception in your catch block and specify the base exception:

# Function that will throw a System.IO.FileNotFoundExceptiopn
function Fail-Read {
  [System.IO.File]::ReadAllLines( 'C:\nonexistant' )
}

# Try to run the function
try {
  Fail-Read
} catch {
  # Throw a new exception, specifying the inner exception
  throw ( New-Object System.Exception( "New Exception", $_.Exception ) )
}

# Check the exception here, using getBaseException()
$error[0].Exception.getBaseException().GetType().ToString()
like image 93
Bender the Greatest Avatar answered Nov 15 '22 16:11

Bender the Greatest


Unfortunately when throwing a new exception from the catch block as described by this answer, the script stack trace (ErrorRecord.ScriptStackTrace) will be reset to the location of the throw. This means the root origin of the inner exception will be lost, making debugging of complex code much harder.

There is an alternative solution that uses ErrorRecord.ErrorDetails to define a high-level message and $PSCmdlet.WriteError() to preserve the script stack trace. It requires that the code is written as an advanced function cmdlet. The solution doesn't use nested exceptions, but still fulfills the requirement "to catch an error and wrap it into something more meaningful for the end user".

#------ High-level function ----------------------------------------------

function Start-Foo {
    [CmdletBinding()] param()

    try {
        # Some internal code that throws an exception
        Get-ChildItem ~foo~ -ErrorAction Stop
    }
    catch {
        # Define a more user-friendly error message.
        # This sets ErrorRecord.ErrorDetails.Message
        $_.ErrorDetails = 'Could not start the Foo'

        # Rethrows (if $ErrorActionPreference is 'Stop') or reports the error normally, 
        # preserving $_.ScriptStackTrace.
        $PSCmdlet.WriteError( $_ )
    }
}

#------ Code that uses the high-level function ---------------------------

$DebugPreference = 'Continue'   # Enable the stack trace output

try {
    Start-Foo -ErrorAction Stop
}
catch {
    $ErrorView = 'NormalView'   # to see the original exception info
    Write-Error -ErrorRecord $_
    ''
    Write-Debug "`n--- Script Stack Trace ---`n$($_.ScriptStackTrace)" -Debug
}

Output:

D:\my_temp\ErrorDetailDemo.ps1 : Could not start the Foo
+ CategoryInfo          : ObjectNotFound: (D:\my_temp\~foo~:String) [Write-Error], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,ErrorDetailDemo.ps1

DEBUG:
--- Script Stack Trace ---
at Start-Foo, C:\test\ErrorDetailDemo.ps1: line 5
at , C:\test\ErrorDetailDemo.ps1: line 14

Our high-level error message 'Could not start the Foo' hides the error message of the underlying exception, but no information is lost (you could access the original error message through $_.Exception.Message from within the catch handler).

Note: There is also a field ErrorDetails.RecommendedAction which you could set as you see fit. For simplicity I didn't use it in the sample code, but you could set it like this $_.ErrorDetails.RecommendedAction = 'Install the Foo'.

like image 23
zett42 Avatar answered Nov 15 '22 17:11

zett42