Mastering error handling in PowerShell: understand the difference between terminating and non-terminating errors, build solid try-catch-finally blocks, choose between throw and Write-Error, control behavior through automatic variables, and apply best practices for production-ready scripts.

In episode 11 you mastered regular expressions — text patterns for filtering, validating, and extracting data. But there's one fact you can't avoid: no matter how good your patterns are, scripts can still fail. Files go missing, servers die, permissions are denied. The right question isn't whether your script will error, but how you respond to it.
This episode covers error handling thoroughly: terminating versus non-terminating errors, the anatomy of error records and exceptions, try/catch/finally blocks, choosing between throw and Write-Error, controlling behavior via $ErrorActionPreference and -ErrorAction, and the error history in $Error.
Imagine sending a box of ten packages through a courier. There are two different kinds of failure. The first: one package in the box is dented — the courier notes it, but still delivers the other nine packages. The second: the address on the whole box is wrong — the delivery stops entirely.
Those two failures map exactly to the two error types in PowerShell:
Get-Content for several files where one can't be opened. The command doesn't stop; it shows the error and still reads the other files.throw, or a .NET exception like dividing a number by zero. Without handling, the script stops at that point.Why does this distinction matter? Because try/catch blocks only reliably catch terminating errors. Non-terminating errors usually "leak" past try/catch uncaptured — this is the biggest source of confusion. Watch the difference when running:
Get-Content file-ada.txt, file-hilang.txt
Write-Host "Baris ini tetap dicetak"throw "Koneksi database gagal"
Write-Host "Baris ini tidak pernah dicetak"In the first example, Get-Content shows an error for the missing file, but Write-Host still runs. In the second, throw stops the script so the next line is never executed.
Every error in PowerShell is an ErrorRecord object — a package containing the message, error category, target object, and the underlying exception. Both non-terminating and terminating errors produce error records; only their behavior toward the pipeline differs.
An exception is the .NET object wrapped inside the record. The Exception property holds the cause details, and the exception type — for example System.IO.FileNotFoundException — is the key to catching specific errors. To see the details: check the $Error[0] automatic variable, which always stores the most recent error — covered in code form later in the episode.
PowerShell's error handling structure is a try block wrapping risky code, catch to respond to failure, and finally for cleanup. The most basic form:
try {
$isi = Get-Content "/etc/hosts" -ErrorAction Stop
Write-Host "Berhasil membaca $($isi.Count) baris"
}
catch {
Write-Host "Gagal membaca file: $($_.Exception.Message)"
}Inside the catch block, the $_ automatic variable points to the error record being handled — that's where $_.Exception.Message comes from.
Think of it like an alarm system: one big alarm that sounds for every disturbance is hard to deal with because you don't know the source of the problem. Better to recognize each exception type one by one, then let one general catch act as the final safety net.
try {
$data = Invoke-RestMethod "https://api.contoh.com/data" -ErrorAction Stop
}
catch [System.Net.WebException] {
Write-Host "Masalah jaringan - cek koneksi."
}
catch [System.UnauthorizedAccessException] {
Write-Host "Token tidak valid."
}
catch {
Write-Host "Error tak terduga: $($_.Exception.Message)"
}catch blocks are checked from top to bottom. Write the most specific blocks first, and keep the typeless catch at the very end as a fallback.
The finally block runs always, whether try succeeds or catch catches an error. This is the right place for cleanup: closing connections, deleting temporary files, or restoring state. Like an eraser that removes marks whether the writing was good or bad.
$file = [System.IO.File]::OpenWrite("/tmp/catatan.txt")
try {
$file.WriteByte(65)
}
catch {
Write-Host "Gagal menulis ke file."
}
finally {
$file.Close()
Write-Host "Handle file ditutup."
}If try succeeds, catch is skipped; if an error occurs, catch handles it — and in both cases finally still runs. Without finally, an open resource hangs and becomes a leak.
Sometimes one level isn't enough: you need to handle failures at a certain stage locally, yet still let more serious errors bubble up to the top level. Like a doorman who handles ordinary guests but calls security for a dangerous one.
try {
Copy-Item "/data/produksi.db" "/backup/produksi.db" -ErrorAction Stop
try {
Invoke-Sqlcmd -Query "SELECT 1" -ErrorAction Stop
}
catch {
Write-Host "Query gagal, mencoba lagi..."
}
}
catch {
Write-Host "Backup gagal - membatalkan operasi."
throw
}Note the throw without arguments in the outer catch — it re-sends the error being handled to the next caller. This is the rethrow pattern: log at this layer, let the upper layer decide the final fate.
Two main ways to raise a custom error: throw and Write-Error.
throw produces a terminating error — suitable for code that really must stop (functions, validation logic). It's also the means of rethrow inside catch.Write-Error writes an error record to the error stream without stopping execution — the error is non-terminating. Suitable for reporting a problem with one item while still continuing other processing.function Test-Port {
param($Port)
if ($Port -lt 1 -or $Port -gt 65535) {
throw "Port $Port di luar rentang 1-65535"
}
Write-Output "Port $Port valid"
}Rule of thumb: use throw for failures that must stop the flow, and Write-Error for one-item failures that can be skipped.
Besides deciding at the per-command level, you can change the default behavior of non-terminating errors at the session level through the automatic variable $ErrorActionPreference.
| Value | Behavior |
|---|---|
| Continue | Show the error, continue (default) |
| SilentlyContinue | Hide the error, continue |
| Stop | Treat all errors as terminating |
| Ignore | Hide the error and don't record it in $Error |
A command's -ErrorAction value (for example Get-Item -ErrorAction SilentlyContinue) always overrides the global preference for that one call only. Ignore is unique because the error is neither displayed nor recorded in $Error — the most resource-efficient if you genuinely don't care about that error.
Important
Never set $ErrorActionPreference = "Stop" without reason in production scripts. This global change makes other commands behave as terminating too — including ones that were originally safe. As a result, a single missing file can stop an entire pipeline. If you only need terminating errors on one command, use -ErrorAction Stop explicitly, or limit the change inside a small block and restore the previous value.
A PowerShell session stores the entire error history in the automatic variable $Error, which is a list. By convention: $Error[0] always holds the most recent error, $Error[1] an older one, and so on — the index runs backwards in time.
Get-Item file-tidak-ada.txt -ErrorAction SilentlyContinue
$Error.Count
$Error[0].Exception.Message
$Error.Clear()
$Error.Count$Error.Clear() empties the history — useful at the start of a test or script so only errors from that session are counted.
Practices you can apply today:
-ErrorAction Stop when you want try/catch to catch errors from cmdlets — without it, most cmdlet errors are non-terminating and slip past catch.catch from the most specific type to the general catch at the very end.finally for resources — files, connections, sessions — so they don't leak when an error occurs.$ErrorActionPreference = "Stop" globally in production scripts.This episode turns errors from a mysterious enemy into a controlled event: you now understand terminating errors that stop execution versus non-terminating ones that only report; dissect error records and exceptions; build try/catch/finally with specific exception catching, finally for cleanup, and nested try-catch with the rethrow pattern; choose throw or Write-Error by context; control global behavior via $ErrorActionPreference and the -ErrorAction parameter; and read the error history with $Error.
Key takeaways:
try/catch only catches terminating errors — add -ErrorAction Stop when needed.finally is guaranteed to run; that's where resource cleanup goes.catch from specific to general exceptions.$Error[0] is the most recent error; $Error.Clear() clears the history.A resilient script is one that knows what to do when everything goes wrong. In episode 13 we organize your code into shareable units: PowerShell Modules — script modules, manifests, the PowerShell Gallery, and how to create your own modules. See you in episode 13!