More concise error messages

This topic contains 10 replies, has 4 voices, and was last updated by  Fausto Nascimento 2 years, 5 months ago.

  • Author
    Posts
  • #24628

    Juho Lehto
    Participant

    When you create scripts for people who only know how to run scripts they have been instructed to, those regular error messages in PowerShell are very scary and confusing.

    Case in point, try to run Do-Something and you receive following error:

    Do-Something : The term 'Do-Something' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
    At line:1 char:1
    + do-something
    + ~~~~~~~~~~~~
        + CategoryInfo          : ObjectNotFound: (do-something:String) [], CommandNotFoundException
        + FullyQualifiedErrorId : CommandNotFoundException
    

    That's not going to work, it scares and confuses people. Everything starting from "At line" is unnecessary noise which is not helpful in the least. These people are going to only see large batch of red noise and have very difficult time finding the actual, useful error message from it.

    PowerShell should have the option to display more concise error messages, like so:
    Do-Something : The term 'Do-Something' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

    That's all you need for an error, all the extra noise still remains accessible through $Error variable.

    So I had to create a function that captures errors and displays concise error message as to avoid unnecessary panic and confusion, which is pasted further below. Unfortunately it takes quite a bit of effort to hide standard error messages. You can either use $ErrorActionPreference = 'SilentlyContinue', which then proceeds to hide all errors, which may not be what you want. Or you can use -ErrorAction 'SilentlyContinue' parameter. That however does not catch terminating errors, so you also need to wrap the command inside Try { }.

    Finally the function needs to be wrapped inside If (-not $?) {Get-ErrorMessage} because by default Catch {} catches only terminating errors. In my experience changing all errors to be terminating errors is just painful, so I never do that. This is probably the most annoying thing I have to do to make my scripts work for people who really only know what they've been instructed.

    I would even like to use parameter validation more, but even those output so much noise that it simply scares people. I always end up handling parameter validation inside the script so that I can output concise errors that do not scare people. There are plenty of other examples, but I think you all get my point by now.

    This is a lot of work, but I don't really see any other way to accomplish this. Do you?

    I know there is also CategoryViews for error messages, but they're not really suitable for the target audience either.

    Function Get-ErrorMessage
    {
    	
    	
    	[CmdletBinding()]
    	param (
    		[Parameter(Position = 0, ValueFromPipeline = $true)]
    		[System.Management.Automation.ErrorRecord]$ErrorRecord
    	)
    	
    	If (-not $local:ErrorRecord)
    	{
    		[System.Management.Automation.ErrorRecord]$ErrorRecord = $global:Error[0]
    	}
    	
    	If ((($ErrorRecord.Exception | Get-Member -MemberType 'Property' | Select-Object -ExpandProperty Name) -contains 'InnerException') -and ($ErrorRecord.Exception.InnerException))
    	{
    		Write-Output $ErrorRecord.Exception.InnerException.Message
    	}
    	Else
    	{
    		Write-Output $ErrorRecord.Exception.Message
    	}
    }
    
  • #24631

    Dave Wyatt
    Moderator

    The concept is fine, I guess. If, in your "controller scripts", you decide that the target audience can't handle normal error text for some reason, go nuts. I wouldn't ever do this kind of thing in a module that I intend to distribute to others, though.

    Also, I don't care for using Write-Output to send error messages. If the caller if your script assigns the results to a variable (or pipes to Out-Null, or whatever), the error messages wind up in there along with any valid output, instead of being displayed on the screen. At the very least, use Write-Warning, or Write-Host -ForegroundColor Red, so the errors are always displayed at the console.

  • #24643

    Juho Lehto
    Participant

    Obviously this is not for distribution for wider audience, because the wider audience is expected to know at least basics of PowerShell if they intend to use the distributed module. Unfortunately that is not true for internal use in companies, where target audience expects things to "just work". Basically, launch PowerShell, type this and wait for results. They wouldn't even know how to get and install a PowerShell module, everything needs to be ready to be used.

    Which is why I had to do my own PowerShell framework which automatically downloads available PowerShell modules, auto-updates them, removes obsolete modules, cleans up error output by using Get-ErrorMessage, loads modules, establishes remote sessions, provides logging functions. etcetera.

    As for outputting the error, I've done it like this: Write-Host "$(Get-ErrorMessage)" -ForegroundColor 'Red'

    And yes, I use Write-Host extensively because it's the only way I can present information to this target audience. White text for progress information, green text for success, red for error. Obviously there are also log-files. A lot of puppies are killed daily.

  • #24644

    Jason Wasser
    Participant

    How about Write-Host $Error[0].Exception.Message -foregroundcolor Red? That's what I've used.

  • #24657

    Juho Lehto
    Participant

    As you can see in the Get-ErrorMessage function, it prefers InnerException.Message over Exception.Message. And that's because Exception.Message displays more information than is needed when there is InnerException defined.

  • #24679

    Fausto Nascimento
    Participant

    Finally the function needs to be wrapped inside If (-not $?) {Get-ErrorMessage} because by default Catch {} catches only terminating errors. In my experience changing all errors to be terminating errors is just painful, so I never do that. This is probably the most annoying thing I have to do to make my scripts work for people who really only know what they've been instructed.

    I'm not exactly sure when you're calling if (-not $?) {Get-ErrorMessage}, but remember taht $? only holds the success/failure of the [b]last[/b] statement. This means that if I do for example...

    function T{
        Get-Service asdasd
        'Hello World'
    }
    T
    $?

    It will return $true, since the last statement was 'Hello World' and that was executed successfully (even though it failed on the Get-Service). So, in short, you'd have to put a if (-not $?) {Get-ErrorMessage} after [b]every[/b] statement that could potentially fail with a non-terminating error.

    I personally find it more painful to have a method for catching terminating errors (try..catch) plus a method for suppressing non-terminating errors (-ErrorAction SilentlyContinue) plus a method for reporting back non-terminating errors (if (-not $?) {Get-ErrorMessage}) than simply having just one try..catch and forcing errors to be terminating.

    This being said... you will find problems with forcing your errors to be terminating, even inside a try..catch, here's an example:

    try
    {
        'DoesntExist', 'spooler' | Get-Service -ErrorAction Stop
    }
    catch
    {
        Write-Host $_ -ForegroundColor Red
    }
    

    So Get-Service first tries to retrieve a service with name 'DoesntExist' and fails. Normally this would be a non-terminating error and it would move to the next service name, but here since we said we want it to behave like a terminating error, it doesn't even try the next one.

    Lastly, am I missing something, or are both your conditions here doing exactly the same thing:

    	If ((($ErrorRecord.Exception | Get-Member -MemberType 'Property' | Select-Object -ExpandProperty Name) -contains 'InnerException') -and ($ErrorRecord.Exception.InnerException))
    

    You're checking if your $ErrorRecord.Exception contains a property of name 'InnerException' and if so, checking if $ErrorRecord.Exception.InnerException is not $null. since in PowerShell you can invoke non-existent properties, you can just do this:

    if ($ErrorRecord.Exception.InnerException)

    If $ErrorRecord.Exception does not contain a property called InnerException, that will return $null which in turn will cause your if to evaluate to $false, and if it does exist and contains a value, it will return $true.

  • #24680

    Juho Lehto
    Participant

    Yep, fully aware $? checks for the last command that was run.

    And yes. you can check for properties that do not exist, unless you have StrictMode enabled. Which I always enable. While StrictMode forces you to check that a property exists before you can check its value, it finds so many potential bugs its totally worth it.

  • #24681

    Fausto Nascimento
    Participant

    You know your environment better than me, but it sounds to me like currently you're building something that is a pain in the neck to maintain by anyone, specially anyone that is not you.

    What happens if next year you decide to get a different job and they need someone else to update the scripts? Unless the new guy uses *exactly* the same methodology you did, it will be a nightmare for everyone involved, people using the script and guy maintaining it alike.

    Would it not be better to educate the users on how to read errors? You are going against everything that PowerShell was designed around to make it into something else.

    That's like saying you want Windows 8, but you don't want it to look and act exactly like Windows XP. With a lot of effort it can be done, but jeez can you imagine the amount of work required to created it and to maintain it?

  • #24815

    Fausto Nascimento
    Participant

    I just remembered... if you want more concise errors, why not set the $ErrorView to 'CategoryView' ?

    That will give you a one liner error, which is what you're after. This already exists in PowerShell.

  • #24832

    Juho Lehto
    Participant

    I did mention CategoryView in the first message. If I simply want the error message string and nothing more, then CategoryView is actually worse.

    I've since revised my scripts to include Set-StrictMode and $ErrorActionPreference which is set to Stop. I can catch errors with fewer Try/Catch statements than before, and no need for $? or ErrorAction parameters. The downside is that I have to work around parts where terminating errors are acceptable. But in the end, this way needs fewer lines of code and less nesting. Which is good. It is at a point where I don't even mind having to call Get-ErrorMessage anymore.

  • #24847

    Fausto Nascimento
    Participant

    Ohh sorry must have missed that, it's been a while since I read the original thread 🙂

    So you're interesting in all types of errors I take it? Terminating and non-terminating, cmdlets/function errors and .NET exceptions?

You must be logged in to reply to this topic.