PowerShell Custom Error Classes and Structured Exception Handling

PowerShell Custom Error Classes and Structured Exception Handling

PowerShell Tips Editor 5 min read
PowerShell Custom Error Classes and Structured Exception Handling

Why Generic Errors Make Debugging Painful

When a PowerShell module throws a string error or calls Write-Error "something went wrong", the caller gets a System.Management.Automation.ErrorRecord wrapping a generic RuntimeException. There is no type to catch, no structured properties to inspect, and no way for callers to distinguish a configuration error from a network timeout. PowerShell 5’s class keyword changes this: you can define typed exception classes that inherit from System.Exception, carry structured properties, and integrate with the standard try/catch mechanism — making automation genuinely debuggable.

Quick Answer

Define a class inheriting System.Exception with the class keyword, add custom properties, instantiate and throw with throw [MyException]::new(), and catch specifically with catch [MyException] in the calling code.

Defining a Custom Exception Class That Inherits System.Exception

PowerShell 5+ classes support single inheritance from .NET types. Inheriting System.Exception means your type integrates naturally with try/catch and the $_.Exception automatic variable inside catch blocks.

class DeploymentException : System.Exception {

    DeploymentException([string]$message) : base($message) {}

    DeploymentException([string]$message, [System.Exception]$inner)
        : base($message, $inner) {}
}

# Basic throw and catch
try {
    throw [DeploymentException]::new('Target environment not reachable')
}
catch [DeploymentException] {
    Write-Host "Caught deployment error: $($_.Exception.Message)"
}

The : base($message) syntax calls the parent constructor and ensures $_.Exception.Message is populated correctly — without it, Message is empty even when you pass a string.

Adding Custom Properties to the Exception

Typed properties on the exception class let callers extract structured context without parsing message strings. Add properties with their .NET types directly in the class body, and populate them through constructors.

class DeploymentException : System.Exception {

    [string]   $TargetEnvironment
    [int]      $ExitCode
    [string[]] $FailedComponents

    DeploymentException(
        [string]$message,
        [string]$targetEnvironment,
        [int]$exitCode,
        [string[]]$failedComponents
    ) : base($message) {
        $this.TargetEnvironment  = $targetEnvironment
        $this.ExitCode           = $exitCode
        $this.FailedComponents   = $failedComponents
    }
}

try {
    throw [DeploymentException]::new(
        'Deployment failed with exit code 1',
        'Production',
        1,
        @('WebApp', 'WorkerService')
    )
}
catch [DeploymentException] {
    $ex = $_.Exception
    Write-Host "Environment : $($ex.TargetEnvironment)"
    Write-Host "Exit code   : $($ex.ExitCode)"
    Write-Host "Failed      : $($ex.FailedComponents -join ', ')"
}

Throwing Typed Exceptions with throw [MyException]::new()

The ::new() static method is the idiomatic way to instantiate PowerShell 5 classes. Combine it with throw to raise the exception. This produces a clean error record with the typed exception accessible via $_.Exception.

function Deploy-Application {
    param([string]$Environment, [string]$Package)

    if (-not (Test-Path $Package)) {
        throw [DeploymentException]::new(
            "Package file not found: $Package",
            $Environment,
            2,
            @('PackageValidation')
        )
    }

    # ... actual deployment logic ...
}

Catching Typed Exceptions with catch [MyException]

PowerShell’s catch blocks match by exact type or any base type in the inheritance chain. This lets calling code differentiate between exception types from the same module and handle each appropriately.

try {
    Deploy-Application -Environment 'Staging' -Package 'C:\Packages\app.zip'
}
catch [DeploymentException] {
    # Structured handling — we have typed properties
    $ex = $_.Exception
    Write-Error "Deployment to '$($ex.TargetEnvironment)' failed (exit $($ex.ExitCode))"
    $ex.FailedComponents | ForEach-Object { Write-Warning "  Component failed: $_" }
}
catch [System.IO.IOException] {
    Write-Error "File I/O error during deployment: $($_.Exception.Message)"
}
catch {
    # Fallback for truly unexpected errors
    Write-Error "Unexpected error: $_"
    throw   # re-throw if caller should see it
}

Exception Hierarchies for Module-Level Error Categories

For modules with multiple error types, define a base exception class and derive specific types from it. Callers can catch the base type to handle any module error, or catch a derived type for specific cases. This mirrors the pattern used throughout the .NET BCL.

class AppException           : System.Exception {
    AppException([string]$m) : base($m) {}
}
class ConfigException        : AppException {
    ConfigException([string]$m) : base($m) {}
}
class NetworkException       : AppException {
    [string]$TargetHost
    NetworkException([string]$m, [string]$host) : base($m) {
        $this.TargetHost = $host
    }
}

Serializing Exception Details for Logging

Structured exceptions can be serialized to JSON for log aggregation. Use ConvertTo-Json on a hashtable built from exception properties. This produces machine-readable log entries that structured logging platforms can index and alert on.

catch [DeploymentException] {
    $logEntry = @{
        Timestamp          = (Get-Date -Format 'o')
        ExceptionType      = $_.Exception.GetType().FullName
        Message            = $_.Exception.Message
        TargetEnvironment  = $_.Exception.TargetEnvironment
        ExitCode           = $_.Exception.ExitCode
        FailedComponents   = $_.Exception.FailedComponents
        StackTrace         = $_.ScriptStackTrace
    }
    $logEntry | ConvertTo-Json -Compress |
        Add-Content -Path 'C:\Logs\deployment-errors.jsonl'
}

Common Errors

  • Custom class defined in a function scope is not visible in the calling script without dot-sourcing. PowerShell class definitions must be at script scope or module scope to be visible to callers. If you define a class inside a function and try to catch it outside, the catch block will not match the type. Define classes at the top level of a script or .psm1 file.
  • Catch block order matters — catch the most specific types first. If you place a catch [System.Exception] block before catch [DeploymentException], the base-type block will always match first and the specific block will never execute. Order from most derived to most base.

Related Cmdlets / See Also

Wrapping Up

Custom exception classes elevate PowerShell error handling from informational messages to a typed, structured contract between functions and their callers. Define them at script or module scope, populate meaningful properties, order catch blocks from specific to general, and serialize them for log aggregation — and your automation becomes genuinely debuggable at scale.

Send-Item -To