skip to content

In PowerShell, how do you catch one specific failure type in a catch block rather than everything, and what does the object in $_ actually contain?

level: middleimportance: should knowfreq 44%

answer

  1. filters go on the catch keyword
  2. $_ is not the exception itself
  3. most specific block first
  4. ask the exception for its FullName

basics

~20 s

Put a .NET exception type in brackets after the catch keyword, such as catch [System.IO.IOException], and order blocks from most specific to most general. Inside, $_ is an ErrorRecord holding the exception, target object, category and script stack trace.

solid answer

~40 s

You put a .NET type filter on the `catch` keyword — `catch [System.IO.FileNotFoundException] { ... }` — and you can stack several catch blocks. PowerShell tries them top down and runs the first whose type matches the exception (or a base type of it), so specific filters go first and a bare `catch` last as the fallback. Inside the block, `$_` (also spelled `$PSItem`) is an **ErrorRecord**, not the exception itself: `$_.Exception` is the .NET exception with its `Message` and `InnerException`, `$_.CategoryInfo` and `$_.FullyQualifiedErrorId` describe what the cmdlet reported, `$_.TargetObject` often carries the item that failed, and `$_.ScriptStackTrace` tells you where in your script it happened. In practice I find the right filter by catching broadly once and printing `$_.Exception.GetType().FullName`.

go deeper

for a junior

Know that a bare catch handles anything, and that details about what failed are available through $_ inside the block rather than from a separate variable.

for a middle

Explain typed catch filters, why blocks are ordered most specific first, and that $_ is an ErrorRecord wrapping the exception rather than the exception itself.

for a senior

Demonstrate how you find the right filter in production: inspect $.Exception.GetType().FullName and $.FullyQualifiedErrorId, and keep $_.ScriptStackTrace in the log line you emit.

for a principal

Set the diagnosability bar for a codebase: what every catch block must log from the ErrorRecord, when a broad catch is negligence, and how errors are re-raised without losing the original cause.

## Filtering a catch block A bare `catch` handles every terminating error, which is fine for a script that only needs to log and exit. When different failures deserve different responses, put a .NET type in brackets after the keyword: ```powershell try { Get-Content -Path $path -ErrorAction Stop } catch [System.IO.FileNotFoundException] { "missing: $path" } catch [System.UnauthorizedAccessException] { "no permission: $path" } catch { "unexpected: $($_.Exception.Message)" throw } ``` PowerShell evaluates catch blocks top to bottom and runs the **first** whose filter matches the exception's type or one of its base types. That ordering rule is the whole trick: a filter on a base type placed first swallows everything below it, so specific types go first and the untyped fallback goes last. One catch block can also carry several types, separated by commas: ```powershell } catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException] { ``` ## $_ is an ErrorRecord, not an exception This is the distinction people get wrong. Inside the block, `$_` — equivalently `$PSItem` — is a `System.Management.Automation.ErrorRecord`. It *wraps* the exception; it is not the exception. So `$_.Message` is not the failure message, while `$_.Exception.Message` is. The record carries a whole diagnostic bundle: - `Exception` — the underlying .NET exception, with `Message`, `GetType()` and possibly `InnerException` when one exception was raised while handling another. - `CategoryInfo` — the coarse classification (`ObjectNotFound`, `PermissionDenied`, `InvalidArgument`, …) plus the activity and target names. - `FullyQualifiedErrorId` — a stable string identifying the specific failure inside a specific cmdlet. Often a *better* discriminator than the exception type, because several unrelated conditions can share one exception type. - `TargetObject` — the object being processed when the failure happened. In a loop over inputs this is frequently what tells you which item failed. - `ScriptStackTrace` — where in *your* script the error came from, which the exception's own `StackTrace` (the .NET frames) does not tell you. - `InvocationInfo` — the command line, script name and line number. ```powershell catch { $_.Exception.GetType().FullName # the type to put in a filter $_.FullyQualifiedErrorId $_.ScriptStackTrace } ``` The same `ErrorRecord` is also what lands in `$Error[0]` afterwards, so everything above is available for post-mortem inspection at an interactive prompt. ## Finding the right type in practice Do not guess exception type names — a filter naming a type that never occurs is silently useless, and a mistyped one is worse. The reliable method is empirical: reproduce the failure once with a bare catch, print `$_.Exception.GetType().FullName`, and paste what it prints into the filter. Add `$_.FullyQualifiedErrorId` to the same probe, because sometimes it is the value you actually want to branch on. One consequence of that advice: if your own code does `throw 'some message'`, PowerShell wraps the string in a `System.Management.Automation.RuntimeException`, so callers cannot usefully filter on it. If you want your failures to be filterable, throw a real exception object — `throw [System.ArgumentException]::new('...')`. ## The re-throw pattern A catch block that handles what it understands and swallows what it does not is a debugging nightmare. When a fallback block cannot genuinely handle the error, log it and re-raise: ```powershell } catch { Write-Verbose "unhandled: $($_.ScriptStackTrace)" throw # bare throw re-raises the current error record } ``` A bare `throw` inside a catch block re-raises the current error rather than creating a new one, which preserves the original record. Wrapping instead — `throw [System.InvalidOperationException]::new('sync failed', $_.Exception)` — keeps the original as the `InnerException`, which is the right move when you want to add context without destroying the cause. ## finally alongside the filters `finally` is independent of which catch matched: it runs after the matching block, and also when *no* block matched and the error is on its way up. That is where session teardown, temporary-file cleanup and restoring a changed preference variable belong — not at the end of the `try` block, which never runs when something fails.

  • Why is $_.Message empty inside a catch block when the error clearly had a message?
    Because `$_` is an ErrorRecord, not an exception. The message lives on the wrapped exception, so you want `$_.Exception.Message`. The ErrorRecord's own `ToString()` also renders the message, which is why string interpolation of `$_` often looks right by accident.
  • When would you branch on FullyQualifiedErrorId instead of the exception type?
    When several distinct conditions in a cmdlet surface as the same exception type, the type filter cannot separate them but the error ID can — it identifies the specific failure inside the specific command. It is also stable enough to test against, whereas message text is localised and can change.
  • What does a bare throw inside a catch block do?
    It re-raises the current error record unchanged, preserving the original exception, category and target object for a caller further up. That is the correct way to log-and-propagate; throwing a new string instead discards the diagnostics and replaces them with a RuntimeException wrapping your text.

saying these in an interview costs you the question

  • Treats $_ in catch as the exception object
  • Puts the broadest catch filter first
  • Guesses exception type names instead of inspecting them
  • Swallows unknown errors in a bare catch
  • Branches on the error message text

context