Structured, Actionable Errors in PowerShell: Precise Failures, Faster Fixes
PowerShell gives you powerful primitives for surfacing rich, predictable errors that speed up debugging and make automation safer. By emitting structured terminating errors only when recovery isn’t possible and attaching clear context to every failure, you guide callers toward immediate fixes and keep pipelines clean and deterministic.
In this post, you’ll learn how to craft actionable errors with ErrorRecord (including ErrorId, Category, TargetObject, and a recommended action), when to use ThrowTerminatingError vs Write-Error, and how to test your error behavior. You’ll leave with patterns you can drop into modules, CI tasks, and production runbooks.
Why Structured, Actionable Errors Matter
Actionable means the fix is obvious
- Describe what failed and where: include the resource, path, or input that caused the failure.
- Classify the failure: set a precise
ErrorCategoryso automation can branch (e.g., retry only on transient resource errors). - Tell the caller what to do: surface a RecommendedAction with a short, specific next step.
Keep the pipeline clean
- Use terminating errors when the current operation cannot proceed: configuration missing, invalid input, corrupted state.
- Use non-terminating errors to report issues you can tolerate or skip while continuing the pipeline (log, skip item, continue).
- Favor
$PSCmdlet.ThrowTerminatingError()and$PSCmdlet.WriteError()to preserve rich metadata.
The anatomy of an ErrorRecord
An ErrorRecord carries the structured context automation cares about:
- Exception: the underlying .NET exception (with message and inner exception).
- ErrorId: a stable, machine-friendly identifier (e.g.,
ConfigMissing,Api.Timeout). - Category: an
ErrorCategoryenum (e.g.,ObjectNotFound,InvalidData,PermissionDenied,ResourceUnavailable). - TargetObject: the object or value that caused the error (path, URI, user, key).
- RecommendedAction: a concise remediation hint (often set via
ErrorDetails.RecommendedAction).
Crafting Precise Terminating Errors
Here’s a strict configuration reader that makes failures obvious. It throws only when recovery isn’t possible, and it carries a clear ErrorId, Category, TargetObject, and a RecommendedAction.
function Get-ConfigStrict {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Path
)
if (-not (Test-Path -Path $Path -PathType Leaf)) {
$ex = [System.IO.FileNotFoundException]::new("Config not found: $Path")
$err = [System.Management.Automation.ErrorRecord]::new(
$ex,
'ConfigMissing',
[System.Management.Automation.ErrorCategory]::ObjectNotFound,
$Path
)
# Surface a remediation hint
$err.RecommendedAction = 'Create the file or pass an existing path.'
$PSCmdlet.ThrowTerminatingError($err)
}
try {
$raw = Get-Content -Path $Path -Raw -ErrorAction Stop
$raw | ConvertFrom-Json -Depth 10
} catch {
$ex = [System.Exception]::new(("Invalid JSON in {0}: {1}" -f $Path, $_.Exception.Message), $_.Exception)
$err = [System.Management.Automation.ErrorRecord]::new(
$ex,
'ConfigInvalid',
[System.Management.Automation.ErrorCategory]::InvalidData,
$Path
)
$err.RecommendedAction = 'Fix JSON syntax or regenerate the file.'
$PSCmdlet.ThrowTerminatingError($err)
}
}
# Usage
# $cfg = Get-ConfigStrict -Path '.\settings.json'
Why this works well:
- ErrorId is stable and greppable in logs and tests.
- Category allows automation to branch (e.g., retry only on
ResourceUnavailable). - TargetObject identifies exactly what broke.
- RecommendedAction speeds up human remediation.
- InnerException preserves low-level details without losing your clean, user-facing message.
A portable helper for consistent errors
Standardize how you create errors with a small helper. This keeps format, categories, and remediation consistent across your module.
function New-ErrorRecord {
param(
[Parameter(Mandatory)][System.Exception]$Exception,
[Parameter(Mandatory)][string]$Id,
[Parameter(Mandatory)][System.Management.Automation.ErrorCategory]$Category,
[Parameter()][object]$TargetObject,
[Parameter()][string]$RecommendedAction
)
$err = [System.Management.Automation.ErrorRecord]::new($Exception, $Id, $Category, $TargetObject)
# Ensure ErrorDetails exists so we can set a recommended action consistently
if (-not $err.ErrorDetails) {
$err.ErrorDetails = [System.Management.Automation.ErrorDetails]::new($Exception.Message)
}
if ($RecommendedAction) {
$err.ErrorDetails.RecommendedAction = $RecommendedAction
}
return $err
}
function Throw-ActionableError {
param(
[System.Exception]$Exception,
[string]$Id,
[System.Management.Automation.ErrorCategory]$Category,
[object]$TargetObject,
[string]$RecommendedAction
)
$PSCmdlet.ThrowTerminatingError(
(New-ErrorRecord -Exception $Exception -Id $Id -Category $Category -TargetObject $TargetObject -RecommendedAction $RecommendedAction)
)
}
Throw vs. Write-Error vs. ThrowTerminatingError
throw: Quick, but you lose structured metadata unless you throw anErrorRecord. PreferThrowTerminatingError()for cmdlets/functions with[CmdletBinding()].$PSCmdlet.ThrowTerminatingError($err): Best for precise, terminating failures with metadata.$PSCmdlet.WriteError($err): Report a non-terminating error and continue when recovery is possible (log and skip an item, for example).
Patterns, Testing, and Automation
Pattern: recoverable item-level failure
When processing a collection, prefer non-terminating errors so you can continue with other items while still emitting rich diagnostics.
function Get-UserProfileSafe {
[CmdletBinding()]
param([Parameter(Mandatory)][string[]]$Usernames)
foreach ($u in $Usernames) {
try {
# Simulate a lookup that can fail per item
$profile = Get-ADUser -Identity $u -ErrorAction Stop | Select-Object SamAccountName, Mail
[pscustomobject]@{ User=$u; Mail=$profile.Mail }
} catch {
$ex = [System.Exception]::new("User lookup failed for '$u': " + $_.Exception.Message, $_.Exception)
$err = New-ErrorRecord -Exception $ex -Id 'User.LookupFailed' -Category ([System.Management.Automation.ErrorCategory]::ObjectNotFound) -TargetObject $u -RecommendedAction 'Verify the username or ensure connectivity to directory services.'
# Non-terminating: report error, continue with next user
$PSCmdlet.WriteError($err)
}
}
}
This keeps the pipeline flowing and lets callers decide how to handle item-level failures with -ErrorAction and -ErrorVariable.
Pattern: API call with redaction and retry hint
Avoid leaking secrets in error text. Include a retryable category only when it’s truly transient.
function Invoke-ApiStrict {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Uri,
[Parameter()][int]$TimeoutSeconds = 15
)
try {
$response = Invoke-RestMethod -Uri $Uri -TimeoutSec $TimeoutSeconds -ErrorAction Stop
return $response
} catch {
$isTimeout = $_.Exception -is [System.OperationCanceledException]
$cat = if ($isTimeout) { [System.Management.Automation.ErrorCategory]::ResourceUnavailable } else { [System.Management.Automation.ErrorCategory]::InvalidOperation }
$msg = "Request to $Uri failed: " + $_.Exception.Message
$ex = [System.Exception]::new($msg, $_.Exception)
$action = if ($isTimeout) { 'Retry later; if persistent, increase timeout or check network routing.' } else { 'Fix request parameters or check service availability.' }
Throw-ActionableError -Exception $ex -Id 'Api.RequestFailed' -Category $cat -TargetObject $Uri -RecommendedAction $action
}
}
Conventions that scale
- ErrorId naming: use
Module.Area.Specific(e.g.,MyMod.Config.Missing) to avoid collisions across modules. - Categories: be intentional. Commonly useful ones:
ObjectNotFound,InvalidData,InvalidArgument,PermissionDenied,ResourceUnavailable,AuthenticationError(if available in your PowerShell version, otherwise useSecurityError),Timeout(or map toOperationTimeoutif you model it as data/operation failure). - TargetObject: pass the precise thing that failed (path, key, URI, identity). It dramatically speeds triage.
- No secrets in messages: never print tokens, passwords, or sensitive payloads. Prefer redaction.
Make it testable with Pester
Write tests that assert your ErrorId, category, and remediation hints so regressions are caught in CI.
# Pester 5 example
Describe 'Get-ConfigStrict' {
It 'throws ConfigMissing with a recommended action when the file is missing' {
$path = '.\nope.json'
{ Get-ConfigStrict -Path $path } | Should -Throw
try { Get-ConfigStrict -Path $path } catch {
# FullyQualifiedErrorId usually includes ErrorId and command name
$_.FullyQualifiedErrorId | Should -Match 'ConfigMissing'
$_.CategoryInfo.Category | Should -Be 'ObjectNotFound'
# RecommendedAction can live under ErrorDetails
if ($_.ErrorDetails) {
$_.ErrorDetails.RecommendedAction | Should -Match 'Create the file'
}
}
}
It 'throws ConfigInvalid on bad JSON' {
$tmp = Join-Path $env:TEMP 'bad.json'
Set-Content -Path $tmp -Value '{ invalid-json }'
try { Get-ConfigStrict -Path $tmp } catch {
$_.FullyQualifiedErrorId | Should -Match 'ConfigInvalid'
$_.CategoryInfo.Category | Should -Be 'InvalidData'
}
}
}
Operational tips
- Default to -ErrorAction Stop in automation entry points so unexpected non-terminating errors don’t slip by silently.
- Use
try/catchat boundaries to convert low-level exceptions into your consistent error vocabulary (wrap with inner exceptions). - Log
FullyQualifiedErrorIdandCategoryInfoto correlate failures across distributed runs. - Prefer
CmdletBinding()for advanced functions so you can use$PSCmdlet.ThrowTerminatingError()and$PSCmdlet.WriteError(). - Document error contracts for public functions: list possible ErrorIds, what they mean, and recommended actions.
Checklist: make failures informative
- Is the ErrorId specific and stable?
- Is ErrorCategory correct and useful for automation?
- Does TargetObject identify the broken thing?
- Is there a concise RecommendedAction?
- Did you wrap the original exception as an inner exception for deep context?
- Did you avoid secrets and PII in messages?
With these patterns in place, your scripts become easier to operate, your CI logs become self-explanatory, and your reviewers will thank you. Make your failures precise—and your fixes obvious.
Further reading: PowerShell Advanced Cookbook → https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/
#PowerShell #ErrorHandling #Scripting #BestPractices #DevOps #PowerShellCookbook