0

I am looking to log unhandled exceptions, since my script runs as an automated tool with no one monitoring progress at the console. My thinking is to create my own exception class, which has a constructor that accepts [InvokationInfo] so I can then log the error with file and line number info, and possibly the trace stack as well. The goal is to catch as many exception types as possible and handle them, but have a generic final catch for exceptions that are literally my code failing. It occurs to me that the entire script would be one big try/catch so that literally anything I didn't expect (so long as it's terminating) would get caught and logged. To that end I have this mocked up:

class UnhandledException : Exception {
    UnhandledException () : base () {
    }
    UnhandledException ([System.Management.Automation.InvocationInfo] $InvokationInfo) {
        Write-Host "Unhandled Exception in $([System.IO.Path]::GetFileName($InvokationInfo.ScriptName)) at line $($InvokationInfo.ScriptLineNumber)"
    }
}

CLS
try {
    1/0
} catch [System.IO.IOException] {
    # Handled exception types go here
} catch {
    $unhandledException = [UnhandledException]::new($PSItem.InvocationInfo)
    #throw $unhandledException 
}

This seems to be working well, and I am debating if that final throw is needed, or if I can just as well terminate the script from within the exception, since by definition once I have logged that info, and maybe thrown up toast message about the failure, I will be exiting the script anyway.

My question is, is this an appropriate way to handle exceptions when the script is functioning as a silent command line utility? I really don't want to have a situation where, if the console is showing, that powershell exceptions are visible. I want to handle everything, to the extent I can, quietly and with log files. Those logs could then be sent to me so I could troubleshoot.

That said, I have not found any info on wrapping the entire script in a try/catch. This suggests that "catch everything" is a code smell, but it's also talking more about methods that are consumed by other users, not utility scripts. The bit about std::uncaught_exception() sounds like it might be an option too, if I could have my regular log, for logging actual progress of the script and error in data, inability to access network resources, etc. All of which would be exceptions that I do catch and handle. If I could define a log file that is ONLY for otherwise uncaught exceptions that might be even better, but I also haven't found anything like that for PowerShell. So my approach is my backup plan. Unless I am missing something and this is a horrible idea?

And, to be clear, I am not thinking this would be the ONLY exception handling. This would be the handler of last resort, logging anything that I hadn't otherwise expected, planned for and handled.

EDIT: So, a limitation I have found to trap is that it still only traps terminating errors, and ideally I would like to also get a log of continued errors as well. To that end, I have been exploring redirects I have tried this

function LocalFunction {
    1/0
}

&{
CLS
LocalFunction 
Remove-Item 'Z:\no.txt' -errorAction silentlyContinue

Test-Path 'C:\'
Write-Host 'Continued'

} 2>> c:\errors.txt

This will successfully log the divide by 0 error in the function and the error at Remove-Item when -errorAction is Continue (the default), but when I specially set it to SilentlyContinue it isn't logged. This seems to get me where I want to be, with ANY error that would be seen in the console instead going to the text file. I could then, at the end of processing, test the size of that file, and delete if 0 or provide a toast message if something got logged.

The real question becomes, is the &{} construct around basically the entire script a viable option once it's a 10,000 line script, rather than a little example? And is it a good idea in general? Or is this perhaps something useful during development, but I just need to put on my big boy pants and actually HANDLE every possible error?

EDIT 2: Well, after doing some tests on a branch of my utility, this redirect approach is actually looking REALLY promising. Apparently no impact on performance, and I can even add the contents of my errors log to my regular log to make things easier for users. Curious if anyone has some counter indications?

Also, a little digging suggest that Invoke-Expression might be better, because the & operator creates a child scope, and that might cause problems while Invoke-Expression doesn't. But on the other hand Invoke-Expression is right up there with Regular Expressions in the "Don't do that" hierarchy. Things that make you go hmmmmmm?

Gordon
  • 6,257
  • 6
  • 36
  • 89
  • In the final `catch` block, why not just write the unhandled exception (+ invocation info) to your log file and then `return`? – Mathias R. Jessen Nov 18 '20 at 20:08
  • 1
    There still exists [`trap`](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_trap). While it's primitive and `try`/`catch` is its intended replacement for most scenarios, it does work on a global level without having to necessarily wrap everything, and it won't touch handled exceptions. – Jeroen Mostert Nov 18 '20 at 20:12
  • @mathias-r-jessen It sounds like you are saying skip the custom exception type, and do the single try/catch around everything, and then just log from that final catch? That is an option, but I will have a couple of "main" scripts, that use a shared library, so I though (and didn't mention) to use the exception type so I am implementing that once, not ponce for every main script. – Gordon Nov 18 '20 at 20:14
  • @jeroen-mostert I had forgotten about 'Trap'. To also address Mathias' comment, I guess I could have a shared function that takes the `InvocationInfo` as an argument, and I could call that from the `Trap`. I think I'll mock that up tomorrow and see how it works. – Gordon Nov 18 '20 at 20:17

0 Answers0