Chapter 10Lesson 03~150 minutes

try, catch, finally, throw, and Designing Explicit Failure Paths

Design explicit PowerShell failure paths with try/catch/finally, throw, rethrowing, typed catches, and cleanup that runs on both success and failure.

Learning objectives

  • Explain which errors reach catch and when -ErrorAction Stop is needed.
  • Inspect the current ErrorRecord inside catch.
  • Use throw for unrecoverable conditions and bare throw to rethrow.
  • Use typed catches only when recovery differs by exception type.
  • Place resource cleanup in finally and make cleanup narrow/idempotent.
  • Preserve technical error context while adding operational context.

1. Failure handling starts with the state you must leave behind

Imagine an automation that creates a temporary workspace, reads a required configuration file, transforms data, and then removes the workspace. If the configuration read fails, cleanup still needs to happen. This is the practical reason to learn try, catch, and finally: they make the success path and failure path explicit while giving cleanup its own guaranteed place.

$root = Join-Path ([System.IO.Path]::GetTempPath()) 'ps-try-demo'
New-Item -ItemType Directory -Path $root -Force | Out-Null
try {
    Get-Content -LiteralPath (Join-Path $root 'required.json') -ErrorAction Stop
}
catch {
    Write-Warning "Load failed: $($_.Exception.Message)"
}
finally {
    Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}

The cleanup is not duplicated in separate success and failure branches.

2. catch handles terminating errors

A catch block runs when a terminating error reaches the associated try. Many familiar cmdlet errors are non-terminating by default, so -ErrorAction Stop is often the deliberate bridge that makes them catchable.

try {
    Get-Item -LiteralPath './missing-item' -ErrorAction Stop
    'success path'
}
catch {
    "caught: $($_.FullyQualifiedErrorId)"
}

Without -ErrorAction Stop, a normal non-terminating “item not found” error may be reported while control continues after the command rather than entering this catch.

3. Inside catch, $_ is the current ErrorRecord

try {
    [int]'not-a-number'
}
catch {
    [pscustomobject]@{
        Message       = $_.Exception.Message
        ExceptionType = $_.Exception.GetType().FullName
        ErrorId       = $_.FullyQualifiedErrorId
        Stack          = $_.ScriptStackTrace
    }
}

This is a machine-usable view of failure. You can log selected fields, add business context, or decide whether recovery is possible without discarding the original evidence.

4. throw creates an explicit script-terminating failure by default

Use throw when your own logic discovers a condition that makes safe continuation impossible. It is appropriate after boundary validation or an invariant check, not as a replacement for parameter validation attributes you already learned.

function Get-RequiredConfig {
    param([string]$Path)

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "Required configuration does not exist: $Path"
    }
    Get-Content -LiteralPath $Path -Raw
}

An uncaught throw unwinds the call stack. If a higher layer can add useful context or recover safely, it can catch the error.

5. Rethrow when you add context but cannot recover

try {
    Get-RequiredConfig -Path './missing.json'
}
catch {
    Write-Error "Deployment configuration stage failed for environment=test"
    throw
}

A bare throw inside catch rethrows the current error, preserving its failure identity and context better than replacing it with an unrelated new string-only error. Add diagnostics before rethrowing when the extra context helps operators.

6. Typed catches are useful when recovery depends on the exception category

You can specify .NET exception types on catch blocks. Put more specific catches before broader ones. Do this when different exception categories genuinely require different behavior; otherwise a single catch with ErrorRecord inspection is often easier to maintain.

try {
    [System.IO.File]::ReadAllText((Join-Path ([System.IO.Path]::GetTempPath()) 'missing-demo.txt'))
}
catch [System.IO.FileNotFoundException] {
    'Use documented fallback because the optional file is absent.'
}
catch [System.UnauthorizedAccessException] {
    throw 'Access denied: do not silently substitute fallback data.'
}
catch {
    throw
}

Recovery policy, not syntax cleverness, should determine whether multiple catches improve the design.

7. finally is the cleanup path, whether success or failure occurs

The finally block runs when control leaves the try/catch structure, including when an error occurred. It is the right place to dispose resources, remove temporary state, or restore a changed setting.

$stream = $null
try {
    $path = Join-Path ([System.IO.Path]::GetTempPath()) 'ps-finally-demo.txt'
    $stream = [System.IO.File]::OpenWrite($path)
    # Work with the resource here.
}
finally {
    if ($null -ne $stream) { $stream.Dispose() }
    Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
}
Cleanup should be narrow: Do not let an avoidable cleanup error hide the primary failure. Make cleanup idempotent where possible and handle expected “already absent” states intentionally.

8. Add business context without destroying technical context

An operator benefits from both layers: “which deployment stage failed?” and “what did PowerShell/provider/native tool report?” Replacing every caught error with a generic message such as throw "failed" makes incident diagnosis slower.

try {
    Get-Content -LiteralPath './required.json' -ErrorAction Stop
}
catch {
    Write-Warning "stage=config-load correlation=DEPLOY-42"
    Write-Warning "errorId=$($_.FullyQualifiedErrorId)"
    throw
}

Later lessons will turn this idea into structured logging with correlation IDs and levels.

9. Do not return a fake success object after an unrecovered failure

# Avoid this pattern:
try {
    Get-Content -LiteralPath './required.json' -ErrorAction Stop
}
catch {
    [pscustomobject]@{ Success=$false; Message='failed' }
}
# Caller may forget to inspect Success and keep going.

Result objects can be valid for domain-level outcomes, but an unrecoverable infrastructure failure usually deserves an error/exception path. Choose one contract and document it; do not silently turn exceptions into ordinary-looking Success-stream data.

10. Lab: intentional failure, cleanup, preserved evidence, clear final status

$root = Join-Path ([System.IO.Path]::GetTempPath()) 'ps-exception-lab'
$tempFile = Join-Path $root 'work.tmp'
$failed = $false

try {
    New-Item -ItemType Directory -Path $root -Force | Out-Null
    Set-Content -LiteralPath $tempFile -Value 'temporary work'

    $required = Join-Path $root 'required.json'
    Get-Content -LiteralPath $required -ErrorAction Stop | Out-Null
}
catch {
    $failed = $true
    Write-Warning "stage=config-load"
    Write-Warning "errorId=$($_.FullyQualifiedErrorId)"
}
finally {
    Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}

[pscustomobject]@{
    Failed      = $failed
    CleanedUp   = -not (Test-Path -LiteralPath $root)
    CompletedAt = Get-Date
}
  • Confirm that the intentional missing file enters catch because -ErrorAction Stop is present.
  • Confirm that finally removes the disposable workspace.
  • Change the catch to a bare throw after logging and observe how failure propagates.
  • Explain when a caller should catch this failure and when it should be allowed to terminate the top-level script.

11. Common mistakes and the underlying model

MistakeWhy it is unreliableBetter pattern
Put a non-terminating cmdlet error in try and expect catch automaticallyNon-terminating errors do not enter catch by defaultUse -ErrorAction Stop when failure must be caught
Replace every caught error with throw "failed"Original error details are lost or obscuredLog useful context and rethrow
Duplicate cleanup in every branchOne branch eventually forgets cleanupUse finally
Catch everything and continueAutomation may proceed with invalid stateRecover only when a safe, explicit fallback exists

12. Knowledge check

Question 1. What kind of errors does catch handle?

Question 2. Why is -ErrorAction Stop commonly used inside try?

Question 3. What does a bare throw inside catch do?

Question 4. When does finally run?

Question 5. Why should a catch block avoid replacing every error with a generic string?

13. Summary

Use try/catch for terminating failures and deliberately escalate important non-terminating cmdlet errors with -ErrorAction Stop. Inside catch, inspect the current ErrorRecord; add business context without discarding technical evidence. Use throw for unrecoverable conditions, a bare throw to rethrow the current failure, typed catches only when recovery policies differ, and finally for cleanup that must happen on both success and failure paths.

14. Further reading

Next lesson

Cross the PowerShell/native boundary safely with explicit exit-code policy and CI-aware failure propagation

Continue to Lesson 4, where the chapter builds on this failure model with cross the powershell/native boundary safely with explicit exit-code policy and ci-aware failure propagation.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.