An Introduction to Error Handling in PowerShell


Today’s post (and this blog’s inaugural post!) is An Introduction to Error Handling in PowerShell. We will discuss error types, the $error variable, error action preferences, try/catch blocks, and $lastexitcode.

The first requirement is to understand the types of errors that can occur during execution.

Terminating vs. Non-Terminating Errors:

  • Terminating Error: A serious error during execution that halts the command (or script execution) completely. Examples can include non-existent cmdlets, syntax errors that would prevent a cmdlet from running, or other fatal errors.
  • Non-Terminating Error: A non-serious error that allows execution to continue despite the failure. Examples include operational errors such file not found, permissions problems, etc.

Update 12/13/2013: Writing a cmdlet? For further information regarding how a cmdlet should determine when to throw a terminating error or non-terminating error, MSDN has a nice explanation here.

Update 12/13/2013: Want to know if an error you encountered is terminating or non-terminating? Check to see if the error behavior is affected by changing the $ErrorActionPreference. According to the MSDN documentation here, “Neither $ErrorActionPreference nor the ErrorAction common parameter affect how Windows PowerShell responds to terminating errors (those that stop cmdlet processing).”.

 

The $error variable:

When either type of error occurs during execution, it is logged to a global variable called $error. This variable is a collection of PowerShell Error Objects with the most recent error at index 0. On a freshly initialized PowerShell instance (no errors have occurred yet) the $error variable is ready and waiting as an empty collection: 

  1. PS C:\> $error.GetType()
  2.  
  3. IsPublic IsSerial Name                                     BaseType
  4. ——– ——– —-                                     ——–
  5. True     True     ArrayList                                System.Object
  6.  
  7. PS C:\> $error.Count
  8. 0

 
In the next snippet I have executed a cmdlet that doesn’t exist, throwing an error. If we grab the count on $error, you will notice it has increased to one item. Dumping that object to the pipeline by accessing $error[0] just prints the error we already saw, right back at us.

  1. PS C:\> ThisCmdlet-DoesNotExist
  2. The term ‘ThisCmdlet-DoesNotExist’ is not recognized as the name of a cmdlet, f
  3. unction, script file, or operable program. Check the spelling of the name, or i
  4. f a path was included, verify that the path is correct and try again.
  5. At line:1 char:24
  6. + ThisCmdlet-DoesNotExist <<<<
  7.     + CategoryInfo          : ObjectNotFound: (ThisCmdlet-DoesNotExist:String)
  8.     [], CommandNotFoundException
  9.     + FullyQualifiedErrorId : CommandNotFoundException
  10.  
  11. PS C:\> $error.Count
  12. 1
  13. PS C:\> $error[0]
  14. The term ‘ThisCmdlet-DoesNotExist’ is not recognized as the name of a cmdlet, f
  15. unction, script file, or operable program. Check the spelling of the name, or i
  16. f a path was included, verify that the path is correct and try again.
  17. At line:1 char:24
  18. + ThisCmdlet-DoesNotExist <<<<
  19.     + CategoryInfo          : ObjectNotFound: (ThisCmdlet-DoesNotExist:String)
  20.     [], CommandNotFoundException
  21.     + FullyQualifiedErrorId : CommandNotFoundException

 
There is more available to us than just what is immediately visible. The ErrorRecord is a rich object that contains many useful properties to explore. Try piping the error to get-member (aliased by gm) to see what options we have available to us:

  1. PS C:\> $error[0] | gm
  2.  
  3.    TypeName: System.Management.Automation.ErrorRecord
  4.  
  5. Name                  MemberType     Definition
  6. —-                  ———-     ———-
  7. Equals                Method         bool Equals(System.Object obj)
  8. GetHashCode           Method         int GetHashCode()
  9. GetObjectData         Method         System.Void GetObjectData(System.Runtim…
  10. GetType               Method         type GetType()
  11. ToString              Method         string ToString()
  12. CategoryInfo          Property       System.Management.Automation.ErrorCateg…
  13. ErrorDetails          Property       System.Management.Automation.ErrorDetai…
  14. Exception             Property       System.Exception Exception {get;}
  15. FullyQualifiedErrorId Property       System.String FullyQualifiedErrorId {get;}
  16. InvocationInfo        Property       System.Management.Automation.Invocation…
  17. PipelineIterationInfo Property       System.Collections.ObjectModel.ReadOnly…
  18. TargetObject          Property       System.Object TargetObject {get;}
  19. PSMessageDetails      ScriptProperty System.Object PSMessageDetails {get=& {…

For details on what each property member provides, visit the ErrorRecord MSDN documentation. A couple important highlights:

  • $error[0].InvocationInfo provides details about the context which the command was executed, if available.
  • $error[0].Exception contains the original exception object as it was thrown to PowerShell. If we explore that object (also piped to get-member) we can see important items to pull up like stack trace, source, HResult, InnerException, etc.

Diving into the exception object itself ($error[0].Exception) can provide very important diagnostic details not immediately visible on the top level error record. This is especially useful in troubleshooting third party cmdlets!

  1. PS C:\> $error[0].Exception
  2. The term ‘ThisCmdlet-DoesNotExist’ is not recognized as the name of a cmdlet, f
  3. unction, script file, or operable program. Check the spelling of the name, or i
  4. f a path was included, verify that the path is correct and try again.
  5.  
  6. PS C:\> $error[0].Exception | gm
  7.  
  8.    TypeName: System.Management.Automation.CommandNotFoundException
  9.  
  10. Name                        MemberType Definition
  11. —-                        ———- ———-
  12. Equals                      Method     bool Equals(System.Object obj)
  13. GetBaseException            Method     System.Exception GetBaseException()
  14. GetHashCode                 Method     int GetHashCode()
  15. GetObjectData               Method     System.Void GetObjectData(System.Runt…
  16. GetType                     Method     type GetType()
  17. ToString                    Method     string ToString()
  18. CommandName                 Property   System.String CommandName {get;set;}
  19. Data                        Property   System.Collections.IDictionary Data {…
  20. ErrorRecord                 Property   System.Management.Automation.ErrorRec…
  21. HelpLink                    Property   System.String HelpLink {get;set;}
  22. InnerException              Property   System.Exception InnerException {get;}
  23. Message                     Property   System.String Message {get;}
  24. Source                      Property   System.String Source {get;set;}
  25. StackTrace                  Property   System.String StackTrace {get;}
  26. TargetSite                  Property   System.Reflection.MethodBase TargetSi…
  27. WasThrownFromThrowStatement Property   System.Boolean WasThrownFromThrowStat…
  28.  
  29. PS C:\> $error[0].Exception.StackTrace
  30.    at System.Management.Automation.CommandDiscovery.LookupCommandInfo(String co
  31. mmandName, CommandOrigin commandOrigin)
  32.    at System.Management.Automation.CommandDiscovery.LookupCommandProcessor(Stri
  33. ng commandName, CommandOrigin commandOrigin, Nullable`1 useLocalScope)
  34.    at System.Management.Automation.ExecutionContext.CreateCommand(String comman
  35. d)
  36.    at System.Management.Automation.CommandNode.CreateCommandProcessor(Int32& in
  37. dex, ExecutionContext context)
  38.    at System.Management.Automation.CommandNode.AddToPipeline(PipelineProcessor
  39. pipeline, ExecutionContext context)
  40.    at System.Management.Automation.PipelineNode.Execute(Array input, Pipe outpu
  41. tPipe, ArrayList& resultList, ExecutionContext context)
  42.    at System.Management.Automation.StatementListNode.ExecuteStatement(ParseTree
  43. Node statement, Array input, Pipe outputPipe, ArrayList& resultList, ExecutionC
  44. ontext context)


Error Action Preference:

PowerShell halts execution on terminating errors, as mentioned before. For non-terminating errors we have the option to tell PowerShell how to handle these situations. This is where the error action preference comes in. Error Action Preference allows us to specify the desired behavior for a non-terminating error; it can be scoped at the command level or all the way up to the script level.

Available choices for error action preference:

  • SilentlyContinue – error messages are suppressed and execution continues.
  • Stop – forces execution to stop, behaving like a terminating error.
  • Continue – the default option. Errors will display and execution will continue.
  • Inquire – prompt the user for input to see if we should proceed.
  • Ignore – (new in v3) – the error is ignored and not logged to the error stream. Has very restricted usage scenarios.

Example: Set the preference at the script scope to Stop, place the following near the top of the script file:

  1. $ErrorActionPreference = “Stop”

Example: Set the preference at the cmdlet level to Inquire, add error action switch (or alias EA): 

  1. get-childitem “G:\FakeFolder” -ErrorAction “Inquire”
  2. get-childitem “G:\FakeFolder” -ea “Inquire”


Try/Catch/Finally Blocks:

The Try, Catch, and Finally statements allow us to control script flow when we encounter errors. The statements behave similar to the statements of the same name found in C# and other languages.

The behavior of try/catch is to catch terminating errors (exceptions). This means Non-terminating (operational) errors inside a try block will not trigger a Catch*. If you would like to catch all possible errors (terminating and non-terminating) – then simply set the error action preference to Stop. Remember that Stop error action forces a non-terminating error to behave like a terminating error, which means it can then be trapped in a catch block. Here is an example:

*Update 12/13/2013: In almost all cases, non-terminating errors will not trigger a catch. However I did recently observe a situation where a non-terminating error did trigger a catch block. It wasn’t from a cmdlet, but an exception generated from directly calling a method on a .net object. So keep in mind that behavior might be possible.

  1. try
  2. {
  3.     <#
  4.         Add dangerous code here that might produce exceptions.
  5.         Place as many code statements as needed here.
  6.         Non-terminating errors must have error action preference set to Stop to be caught.
  7.     #>
  8.  
  9.     write-host “Attempting dangerous operation”
  10.     $content = get-content -Path “C:\SomeFolder\This_File_Might_Not_Exist.txt” -ErrorAction Stop
  11. }
  12. catch
  13. {
  14.     <#
  15.         You can have multiple catch blocks (for different exceptions), or one single catch.
  16.         The last error record is available inside the catch block under the $_ variable.
  17.         Code inside this block is used for error handling. Examples include logging an error,
  18.         sending an email, writing to the event log, performing a recovery action, etc.
  19.         In this example I’m just printing the exception type and message to the screen.
  20.     #>
  21.  
  22.     write-host “Caught an exception:” -ForegroundColor Red
  23.     write-host “Exception Type: $($_.Exception.GetType().FullName)” -ForegroundColor Red
  24.     write-host “Exception Message: $($_.Exception.Message)” -ForegroundColor Red
  25. }
  26. finally
  27. {
  28.     <#
  29.         Any statements in this block will always run even if errors are caught.
  30.         This statement block is optional. Normally used for cleanup and
  31.         releasing resources that must happen even under error situations.
  32.     #>
  33.  
  34.     write-host “Finally block reached”
  35. }

 
You can also have Catch blocks that will only trap specific exceptions. The reason for doing this is so you can add different handlers for each possible failure condition that you may encounter. Some exceptions you may just want to log and exit, but others you may have a recovery action for. Here is a Catch statement that would trap a specific Exception type. The Exception type is displayed in brackets after the catch statement:

  1. catch [System.Management.Automation.ItemNotFoundException]
  2. {
  3.     # catching specific exceptions allows you to have
  4.     # custom actions for different types of errors
  5.     write-host “Caught an ItemNotFoundException” -ForegroundColor Red
  6. }

 
You might be wondering how I found the type name for the previous exception. The possible exceptions for cmdlets are not usually documented, so you may need to find them on your own. When an exception occurs you can look up the error in the $error collection, or while inside a catch block under the $_ variable. Call the GetType() method on the base exception to extract the FullName property. Like shown here:

  1. PS C:\> $error[0].Exception.GetType().FullName
  2. System.Management.Automation.ItemNotFoundException


Handling Errors from non-PowerShell processes:

What happens when your script needs to run an external process from PowerShell and you want to know if it succeeded? An example would be a cmdline tool such as robocopy.exe. It’s an external application that returns an exit code upon completion. But since it is an external process, its errors will not be caught in your try/catch blocks.

To trap this exit code utilize the $LastExitCode PowerShell variable.

When the launched process exits, PowerShell will write the exit code directly to $LastExitCode. In most cases an exit code of 0 means success, and 1 or greater indicates a failure. Check the external tool’s documentation to verify of course.

Here it is seen in action:

  1. PS C:\> robocopy.exe “C:\DirectoryDoesNotExist” “C:\NewDestination” “*.*” /R:0
  2.  
  3. ——————————————————————————-
  4.    ROBOCOPY     ::     Robust File Copy for Windows
  5.  
  6. ——————————————————————————-
  7.  
  8.   Started : Sun Jun 09 18:42:09 2013
  9.  
  10.    Source : C:\DirectoryDoesNotExist\\par      Dest : C:\NewDestination\\par
  11.     Files : *.*
  12.  
  13.   Options : *.* /COPY:DAT /R:0 /W:30
  14.  
  15. ——————————————————————————
  16.  
  17. 2013/06/09 18:42:09 ERROR 2 (0x00000002) Accessing Source Directory C:\Directory
  18. DoesNotExist\\par The system cannot find the file specified.
  19. PS C:\> $lastexitcode
  20. 16

 

 

Comments (20)

  1. M says:

    I need to force a powershell script to fail.  

  2. Awesome article on Error Handling!  I'm adding this to my favorites now, thank you for sharing.

  3. D says:

    Thanks for this article!

  4. Nathan says:

    Nice article.  Helped me out a lot.  However, I am now facing another challenge.  I am trying to write the $Error output from above that was going to the console to a txt file.  So my code looks like this:

    $compname = Get-Content -Path C:ServerList.txt

    $date = Get-Date -Format yyyyMMdd_hhmm

    $unit="GB"

    $measure = "1$unit"

    FOREACH ($computerName in $compname)

    {

    TRY

    {

    $ErrorActionPreference = "Stop";

    Get-WmiObject -computername $computerName -query "

    select SystemName, Name, DriveType, FileSystem, FreeSpace, Capacity, Label

     from Win32_Volume

    where DriveType = 2 or DriveType = 3" `

    | select SystemName `

    , Name `

    , @{Label="SizeIn$unit";Expression={"{0:n2}" -f($_.Capacity/$measure)}} `

    , @{Label="FreeIn$unit";Expression={"{0:n2}" -f($_.freespace/$measure)}} `

    , @{Label="PercentFree";Expression={"{0:n2}" -f(($_.freespace / $_.Capacity) * 100)}} `

    ,  Label  | Export-Csv c:SpaceInformation_$date.csv -Append  

    }

    CATCH

    {

    WRITE-HOST "Computer Name: $computerName`nError: $($_.Exception.Message)" > c:errors.txt

    }

    FINALLY

    {

    $ErrorActionPreference = "CONTINUE";

    }

    }

    However, no matter if I use > or OUT-FILE or 2>, it still writes to the console, rather than the file.  

    What am I doing wrong?

    Thanks,

    Nathan

  5. Keith Babinec says:

    @Nathan – I assume that you want to print to the screen and also write to the file? Try removing write-host (leaving the string you want to print), and using the tee-object cmdlet. This should print to the screen and also to the output file.

    Example:

    try

    {

    # your code here

    }

    catch

    {

    "Computer Name: $computerName`nError: $($_.Exception.Message)" | Tee-Object -File c:errors.txt

    }

  6. Alok says:

    G8 Blog, Solve my problem as well. Thank you 🙂

  7. TheMightyC says:

    Great article. I'd like to ask about a problem in one of mscripts, though. I have a script with a try/catch block, and in the catch, I call a function I've written called RollbackEverything. I can call RollbackEveything from the try block with no problem, but when I call it from the Catch block, I see the error "The term 'RollbackEverything' is not recognized as a cmdlet, function, operable program, or script file. Verify the term and try again."

    What is happening, and is there a way to fix it?

  8. Keith Babinec says:

    @TheMightyC – I just tried to reproduce the issue you describe and I'm not seeing it. I even tried to declare a function inside the scope of the try block, and it still was able to be called from the catch block. If you are still having issues, I would recommend posting your code on the MSDN forums or stackoverflow.

    This works just fine on powershell v2 and v3:

    # ——————————-

    function test()

    {

       return "inside test"

    }

    test

    try

    {

       write-host "inside the try block"

       function test2()

       {

           return "inside test2"

       }

       test2

       throw "some error"

    }

    catch

    {

       write-host "inside the catch block"

       test

       test2

    }

    write-host "end"

    # ——————————-

    the code returns the following for me:

    inside test

    inside the try block

    inside test2

    inside the catch block

    inside test

    inside test2

    end

  9. Hien says:

    Great error handling article.  Thank you!

  10. Noor says:

    Awesome Article…. thanks…

  11. Tom Pester says:

    Good article

    FYI You picked Robocopy and that's one of the few that does return a non 0 exit code to signal success

    For example, it returns 3 (making this up) when no files were copied and it still was a successful action

  12. Andrew Stevens says:

    Nice and clear well done

  13. Keith Babinec says:

    @Tom – good point, totally forgot that robocopy has some non-zero success codes.

  14. Ludovic says:

    Great article, with a few very useful tips, like the way to get the correct error type. Thank you for sharing.

  15. Michael Liben says:

    Two thumbs up. Must read.

  16. MB says:

    Really good stuff.  Just what I was looking for.  Very clear.  Thanks.

  17. Ryan Patridge says:

    Agreed, great post.  One question: might there be a good way to detect/log/handle a non-terminating error?  I'd like to leave the $ErrorActionPreference setting alone and allow non-terminating errors to continue in their default fashion, but I'd still like to be able to "catch" them to log/detect them.

  18. Devaraj Totagara says:

    Nice Article Sir. Thanks 🙂

  19. Anon says:

    How do you clear an error intentionally. As in err.clear in vbscript?

  20. Keith Babinec says:

    @Anon, you can clear the error collection itself if you want to…  just call $error.Clear()

    PS C:UsersKeith> $error.Count

    2

    PS C:UsersKeith> $error.Clear()

    PS C:UsersKeith> $error.Count

    0