PowerShell Throw and Custom Exceptions

A function that silently returns $null when it fails puts the burden of failure detection on every caller. Using the PowerShell throw exception pattern shifts that burden correctly: the function signals failure loudly, callers handle it in catch blocks, and error messages describe exactly what went wrong. This post covers throwing string messages, .NET exception objects, creating custom exception classes in PowerShell, catching specific types, and the re-throw pattern that preserves the original stack trace.
Throw a String Message
The simplest form of throw accepts a string. PowerShell wraps it in a RuntimeException automatically and terminates the current scope:
function Get-ConfigFile {
param([string]$Path)
if (-not (Test-Path $Path)) {
throw "Config file not found: $Path"
}
Get-Content $Path | ConvertFrom-Json
}
try {
Get-ConfigFile -Path "C:\Config\app.json"
}
catch {
Write-Host "Failed: $($_.Exception.Message)"
}
Failed: Config file not found: C:\Config\app.json
Throw a .NET Exception Object
Throwing a specific .NET exception type lets callers catch by type rather than by message text, which is far more robust:
function Connect-Database {
param([string]$Server, [string]$Database)
$conn = New-Object System.Data.SqlClient.SqlConnection
$conn.ConnectionString = "Server=$Server;Database=$Database;Integrated Security=True"
try {
$conn.Open()
}
catch [System.Data.SqlClient.SqlException] {
throw [System.Data.SqlClient.SqlException]::new(
"Cannot connect to $Server\$Database: $($_.Exception.Message)"
)
}
return $conn
}
Throw Inside a Function
When a function uses throw, the exception propagates up the call stack until a catch block handles it. If nothing catches it, PowerShell displays the error and stops the script. Always document that a function can throw in its comment-based help .NOTES or .DESCRIPTION:
function Invoke-Backup {
param([string]$Source, [string]$Destination)
if (-not (Test-Path $Source)) {
throw [System.IO.DirectoryNotFoundException]::new("Source not found: $Source")
}
if ((Get-PSDrive -Name ($Destination.Substring(0,1))).Used -gt 0.9) {
throw [System.IO.IOException]::new("Destination drive low on space")
}
Copy-Item -Path $Source -Destination $Destination -Recurse -Force
}
Create Custom Exception Classes
PowerShell 5+ supports defining custom .NET exception classes inline with class syntax. Custom exceptions let callers distinguish your application’s errors from generic .NET exceptions:
class ValidationException : System.Exception {
[string]$FieldName
[object]$InvalidValue
ValidationException([string]$message, [string]$field, [object]$value)
: base($message) {
$this.FieldName = $field
$this.InvalidValue = $value
}
}
function Validate-UserInput {
param([string]$Email)
if ($Email -notmatch '^[\w.+-]+@[\w-]+\.[a-z]{2,}$') {
throw [ValidationException]::new(
"Invalid email format",
"Email",
$Email
)
}
}
try {
Validate-UserInput -Email "not-an-email"
}
catch [ValidationException] {
Write-Host "Validation failed on field '$($_.Exception.FieldName)': $($_.Exception.Message)"
Write-Host "Rejected value: $($_.Exception.InvalidValue)"
}
Validation failed on field 'Email': Invalid email format
Rejected value: not-an-email
Catch Specific Exception Types
Catch blocks can filter by exception type using catch [TypeName]. PowerShell evaluates catch blocks in order and runs the first matching one:
try {
Invoke-Backup -Source "C:\Data" -Destination "D:\Backup"
}
catch [System.IO.DirectoryNotFoundException] {
Write-Host "Source directory missing — check path" -ForegroundColor Yellow
}
catch [System.IO.IOException] {
Write-Host "IO error during backup: $($_.Exception.Message)" -ForegroundColor Red
}
catch {
Write-Host "Unexpected error: $_" -ForegroundColor Red
throw # re-throw to not swallow unknown errors
}
Re-Throw vs Wrap Exceptions
To preserve the original exception and its stack trace, use bare throw inside a catch block (no argument). To wrap the original exception in a more descriptive one, create a new exception with the original as InnerException:
try {
# ... operation that may fail ...
}
catch [System.UnauthorizedAccessException] {
# Re-throw preserving original stack trace
throw
}
catch {
# Wrap with context — original error available via InnerException
$wrapped = [System.InvalidOperationException]::new(
"Backup failed at $(Get-Date): $($_.Exception.Message)",
$_.Exception
)
throw $wrapped
}
Common Errors and Fixes
-
Throwing strings creates RuntimeException, not a custom type.
throw "message"creates aSystem.Management.Automation.RuntimeException. If your callers need to catch specific error types, always throw a .NET exception object or a custom class instance, not a bare string. -
Stack trace lost when re-throwing with an argument.
throw $_(with the error record as argument) creates a new exception and loses the original stack trace. Use barethrowwith no argument to preserve the original context when you want to let an exception bubble up after logging it.
Related Cmdlets / See Also
Wrapping Up
Throw meaningful, typed exceptions so callers can handle failures precisely. Use string throws for simple scripts, .NET exception types for typed catching, and custom exception classes when your application has domain-specific failure modes. Always re-throw with bare throw when you want to preserve the original stack trace for debugging.


