Post

Building A Dedicated Error Handler for PowerShell Automations

Building A Dedicated Error Handler for PowerShell Automations

Introduction

Error handling in PowerShell is typically managed through the try/catch/finally pattern; however, when building complex automations you may start to notice reoccuring error patterns and wonder if extracting them into a dedicated error handeler method would streamline your code or add modularity that you can reuse across future automations.

It’s a good question, and exploring it will give me an opertunity to highlight one of my favorite PowerShell features: using script blocks as hastable values.

Error Domains

Not all errors need to be surfaced to the end user in the same way. Depending on the context, you may want to either provide a clear, user-friendly message or retain the full technical details for debugging. These categories, which we can think of as error domains, define what information should be displayed and how it should be presented.

  • Public errors
    Sanitized messages that can be safely shown to users without exposing sensitive details about your automation.
    Examples:
    • "No open session for this action."
    • "Username or password is incorrect."
  • Private errors
    Detailed messages intended for developers or administrators to help identify the source of bugs. These often include raw exception data or stack traces.
    Example:
    • $Error.Exception

The Enum

There are several ways to define error domains in PowerShell: using if/else blocks, ValidateSet parameters, or in-function switch statements; however, this is an excellent use case for enums.

An enum allows you to define a static set of constants. In practice, enums are collections of predefined, immutable values. and using them adds type safety to your code and makes future extensibility much easier.

Basic Enum Format

1
2
3
4
5
6
7
8
9
10
# Define an enum
enum NAME { Value1; Value2 }

# Use an enum value
$myValue = [NAME]::Value1

# Evaluate against an enum
if ($myValue -eq [NAME]::Value1) {
    Write-Host "Your value is 1"
}

More About Enums

Enums have a wide range of applications in PowerShell, but for error handling my primary goals are type safety and tool support. Additionally, defining enums allows that specified domain values can be enforced, and the IDE will automatically surface those options to the developer.

This approach ensures that error handling remains consistent and predictable. You can also use enums in the same way to define error types, and both domains and types can be expanded to cover additional scenarios; for example, distinguishing between recoverable vs. unrecoverable errors, or separating console vs. GUI domains.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# define error domains
enum ErrorDomain { Public; Private }

# define error types
enum ErrorType   { Info; Error }

# Example usage
function Resolve-Error {
    param(
        [ErrorDomain] $Domain,
        [ErrorType]   $Type
    )

    switch ($Domain) {
        'Public'  { Write-Host "User-friendly message displayed." }
        'Private' { Write-Host "Detailed exception logged: $($Error[0].Exception)" }
    }
}

The ScriptBlock as a Hashtable Value

Hashtables are incredibly useful in PowerShell automation because they let you package custom key/value pairs together and pass them around natively. One common use case is structuring function returns with metadata. e.g. returning both the result of a process and additional information about its execution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# REST request method with structured return that can be unpacked by the caller
function Send-Request ([Hashtable] $Request) {
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    
    try {
        $response = Invoke-RestMethod @Request
        return @{
            success = $true
            data    = $response
            error   = $null
        }
    } catch {
        return @{
            success = $false
            data    = $null
            error   = $_.Exception.Message
        }
    }
}

Error Handling as a Dedicated Process

You can also package script blocks inside a hashtable, which opens up some very powerful dynamic possibilities. One example is building an extensible error handler that adapts its behavior based on the type of error passed into it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
enum ErrorDomain { Public; Private }
enum ErrorType   { Info; Error }

function Resolve-Error {
    [cmdletBinding()]
    param(
        [ErrorDomain] $Domain,
        [ErrorType]   $Type,

        [System.Management.Automation.ErrorRecord] $ErrorRecord,
        [string] $Message
    )

    # define error handling logic based on domain/type pairing
    [hashtable] $handlers = @{
        'public:info'   = [scriptblock] { "This is a $Domain error of type $Type" }
        'public:error'  = [scriptblock] { "This is a $Domain error of type $Type" }
        'private:info'  = [scriptblock] { "This is a $Domain error of type $Type" }
        'private:error' = [scriptblock] { "This is a $Domain error of type $Type" }
    }

    # construct domain/type table key
    $handlerKey = $domain, $Type -Join ':'

    if ($handlers.ContainsKey($handlerKey)) {
         # note the &, which executes the script block of $handlers.handlerKey
        & $handlers[$handlerKey]
    } else {
        throw "no handler for key: $handlerKey"
    }
}

Resolve-Error -Domain public -Type info
Resolve-Error -Domain public -Type error
Resolve-Error -Domain private -Type info
Resolve-Error -Domain private -Type error
Resolve-Error -Domain bad -Type unknown

Expand Error Logic

Now that we’re parsing enums correctly, we can start defining error-handling logic with either custom messages or detailed error output. This pattern is highly flexible, but the general principle is:

  • Public errors should display tailored, user-friendly messages without exposing sensitive details.
  • Private errors should capture and log detailed information for developers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# define error handling logic based on domain/type pairing
enum ErrorDomain { Public; Private }
enum ErrorType   { Info; Error }

function Resolve-Error {
    [cmdletBinding()]
    param(
        [ErrorDomain] $Domain,
        [ErrorType]   $Type,

        [System.Management.Automation.ErrorRecord] $ErrorRecord,
        [string] $Message
    )

    # define error handling logic based on domain/type pairing
     [hashtable] $handlers = @{
        'public:info'   = [scriptblock] { Write-Host "ℹ️ $Message" }

        'public:error'  = [scriptblock] { Write-Host "❌ An error occurred. Please try again." }

        'private:info'  = [scriptblock] { Write-Verbose "Info: $Message" }

        'private:error' = [scriptblock] {
            $logEntry = "[$(Get-Date)] :: ERROR :: $($ErrorRecord.Exception.Message)"
            Add-Content -Path ".\automation-errors.log" -Value $logEntry
            Write-Host "Detailed error logged for review."
        }
    }

    # construct domain/type table key
    $handlerKey = $domain, $Type -Join ':'

    if ($handlers.ContainsKey($handlerKey)) {
        # note the &, which executes the script block of $handlers.handlerKey
        & $handlers[$handlerKey]
    } else {
        throw "no handler for key: $handlerKey"
    }
}

# Example calls
Resolve-Error -Domain Public -Type Info -Message "Operation completed successfully."
Resolve-Error -Domain Public -Type Error -Message "Unable to connect to server."
Resolve-Error -Domain Private -Type Info -Message "Retrying request with new parameters."
Resolve-Error -Domain Private -Type Error -ErrorRecord $Error[0]

Final thoughts

Error handling in PowerShell doesn’t have to be limited to try/catch blocks scattered throughout your scripts. By introducing error domains and error types as enums, you gain type safety, IDE support, and a consistent way to classify errors. Pairing those enums with script blocks stored in hashtables unlocks dynamic, extensible error-handling logic that adapts to the situation at hand.

The result is a dedicated error-handling process that is:

  • Modular logic is centralized and reusable across automations.
  • Extensible new domain/type pairs can be added without rewriting core functions.
  • Safe enums prevent invalid values, while unhandled cases are surfaced immediately.
  • Practical public errors remain user-friendly, while private errors provide developers with actionable detail.

This approach not only streamlines your automation code but also makes it easier to maintain, debug, and scale. Whether you’re building small scripts or complex automation frameworks, treating error handling as a first-class, dedicated process will pay dividends in reliability and clarity.

This post is licensed under CC BY 4.0 by the author.