TB

MoppleIT Tech Blog

Welcome to my personal blog where I share thoughts, ideas, and experiences.

Debug Faster with PowerShell Get-Error: Turn Vague Failures into Actionable Fixes

PowerShell 7 introduced Get-Error, a purpose-built cmdlet that turns cryptic red text into a rich, structured breakdown of what failed, where it failed, and why. Instead of scanning walls of output, you can surface the inner exception, call stack, command info, and even HResult codes in seconds. In this post, you'll learn practical patterns to use Get-Error for rapid root cause analysis, clearer logs, and reliable automation.

  • Use Get-Error to expand inner exceptions and call stacks.
  • Pipe the latest error (or a specific ErrorRecord) for precise details.
  • Capture key fields for quick, structured logs.

Whether you're debugging locally, inside a container, or in CI/CD, these techniques will help you troubleshoot with confidence.

Understand Error Records and Get-Error

ErrorRecord basics

PowerShell errors are objects (ErrorRecord) that carry rich context: the exception, command invocation info, script and line number, category info, and more. You can access recent errors from the automatic $Error variable, e.g. $Error[0] for the most recent one.

Get-Error takes those records and expands them into a developer-friendly, detailed view:

# Show the most recent error with full detail
Get-Error -Newest 1 -Detailed

# Pipe a specific ErrorRecord
$Error[0] | Get-Error -Detailed

# Control how deep to expand inner exceptions
Get-Error -Newest 1 -Detailed -Depth 5

# When you only want messages (no stacks)
Get-Error -Newest 3 -Message

Get-Error is available in PowerShell 7+. If you're on Windows PowerShell 5.1, consider upgrading or running PowerShell 7 side-by-side.

Expanding inner exceptions and call stacks

Many failures hide the true cause inside InnerException. With -Detailed (and optionally -Depth), Get-Error walks the inner exception chain and emits a ScriptStackTrace showing where the failure bubbled up. That makes it easy to pinpoint the first frame that matters and skip noisy infrastructure calls.

Fast workflows you can copy-paste

Turn the last error into an actionable summary

This pattern captures the message, HResult, last command, and the top stack frame so you can see the failure at a glance. Paste it into any catch block:

try {
  Invoke-RestMethod -Uri 'https://bad.example/api' -ErrorAction Stop
} catch {
  $e = Get-Error -ErrorRecord $_ -Detailed
  [pscustomobject]@{
    Message  = $e.ErrorRecord.Exception.Message
    HResult  = ('0x{0:X}' -f $e.ErrorRecord.Exception.HResult)
    LastCmd  = $e.InvocationInfo.MyCommand
    StackTop = ($e.ScriptStackTrace -split '\r?\n' | Select-Object -First 1)
  } | Format-List
}

Use this anytime you want a concise, human-readable snapshot during dev or when tailing logs in a container shell.

Structured JSON logs for CI/CD and containers

In automation, structured logs beat free-form text. Emit JSON so your pipeline, log shipper, or SIEM can search and alert on the exact fields you care about.

try {
  # Your script logic here
  throw [System.InvalidOperationException]::new('Bad config value: sizeLimit')
} catch {
  $e = Get-Error -ErrorRecord $_ -Detailed -Depth 5
  $payload = [pscustomobject]@{
    Timestamp = (Get-Date).ToString('o')
    Message   = $e.ErrorRecord.Exception.Message
    Exception = $e.ErrorRecord.Exception.GetType().FullName
    HResult   = ('0x{0:X}' -f $e.ErrorRecord.Exception.HResult)
    FQID      = $e.FullyQualifiedErrorId
    Command   = $e.InvocationInfo.MyCommand
    Script    = $e.InvocationInfo.ScriptName
    Line      = $e.InvocationInfo.ScriptLineNumber
    Stack     = $e.ScriptStackTrace
  }

  $json = $payload | ConvertTo-Json -Depth 6
  # Append to a file or send to your collector
  $json | Out-File -FilePath './error.log' -Append -Encoding utf8
  throw  # rethrow if you want the job to fail
}

Tip: include a stable FQID (FullyQualifiedErrorId) to group similar failures across runs.

HTTP and API failures: pull status code and body

REST calls often require peeking into the response to understand the root cause. Depending on the exception type, you may have Response, StatusCode, or other properties. This pattern extracts them when available:

try {
  Invoke-RestMethod -Uri 'https://api.example.com/thing' -Method Post -Body @{ name = 'x' } -ErrorAction Stop
} catch {
  $e = Get-Error -ErrorRecord $_ -Detailed
  $status = $null; $body = $null

  if ($e.ErrorRecord.Exception -is [Microsoft.PowerShell.Commands.HttpResponseException]) {
    $status = [int]$e.ErrorRecord.Exception.Response.StatusCode
    $body   = $e.ErrorRecord.Exception.Response.Content
  } elseif ($e.ErrorRecord.Exception -is [System.Net.WebException]) {
    $resp   = [System.Net.HttpWebResponse]$e.ErrorRecord.Exception.Response
    $status = [int]$resp.StatusCode
    $sr     = New-Object System.IO.StreamReader($resp.GetResponseStream())
    $body   = $sr.ReadToEnd(); $sr.Dispose()
  }

  [pscustomobject]@{
    StatusCode = $status
    Body       = $body
    Message    = $e.ErrorRecord.Exception.Message
    StackTop   = ($e.ScriptStackTrace -split '\r?\n' | Select-Object -First 1)
  } | Format-List
}

Now you can log the HTTP status and response body alongside the message and top stack frame for quick triage.

A reusable helper: Get-LastErrorSummary

Drop this function in your profile or shared module to quickly summarize recent failures from the interactive console or a failing job.

function Get-LastErrorSummary {
  param(
    [int]$Newest = 1,
    [int]$Depth  = 3
  )
  $e = Get-Error -Newest $Newest -Detailed -Depth $Depth
  [pscustomobject]@{
    Message  = $e.ErrorRecord.Exception.Message
    Type     = $e.ErrorRecord.Exception.GetType().FullName
    HResult  = ('0x{0:X}' -f $e.ErrorRecord.Exception.HResult)
    Command  = $e.InvocationInfo.MyCommand
    Script   = $e.InvocationInfo.ScriptName
    Line     = $e.InvocationInfo.ScriptLineNumber
    StackTop = ($e.ScriptStackTrace -split '\r?\n' | Select-Object -First 1)
  }
}

# Usage
Get-LastErrorSummary | Format-List

Make it production-ready

Best practices that surface the real root cause

  • Promote to terminating errors when appropriate: use -ErrorAction Stop on cmdlets so exceptions land in catch.
  • Prefer try { } catch { } to trap for clarity and scope control.
  • Capture a dedicated error variable: -ErrorVariable ev lets you inspect $ev separately from the global $Error buffer.
  • Fail fast in automation: after logging with Get-Error, throw to signal job failure if required.
$ev = @()
Invoke-Something -ErrorAction Continue -ErrorVariable ev
if ($ev.Count -gt 0) {
  $ev[0] | Get-Error -Detailed | Out-String | Write-Host
}

Integrate with CI/CD and containers

  • Containers: Write structured logs to stdout or a mounted volume so your orchestrator (Docker, Kubernetes) can collect them. Example: $payload | ConvertTo-Json -Depth 6 | Write-Output.
  • GitHub Actions/Azure DevOps: Emit concise summaries to the console and attach full JSON as an artifact. Keep stack traces searchable.
  • Retry loops: On transient failures, log the first and last occurrence with Get-Error and include a correlation ID for traceability.

Security and signal quality

  • Redact secrets: Before logging response bodies or headers, mask tokens and passwords. Keep the structure, remove the secrets.
  • Avoid PII sprawl: Only capture fields you need: message, exception type, status code, top stack frame, and command are usually enough.
  • Control noise: Periodically $Error.Clear() in long-running sessions so Get-Error -Newest references current failures.
function Mask-Secret {
  param([string]$text)
  if ([string]::IsNullOrEmpty($text)) { return $text }
  # Basic masking for common keys (tune to your data)
  return ($text -replace '(?i)(token|apikey|password)\s*=\s*[^;,\s]+', '$1=***')
}

Performance tips

  • Use -Message when you don't need stacks; it's lighter and faster.
  • Limit recursion with -Depth to avoid expanding huge inner exception trees.
  • Log once: capture a summarized object for your pipeline and keep verbose detail only on demand.

A quick troubleshooting checklist

  1. Reproduce and run Get-Error -Newest 1 -Detailed.
  2. Skim the top stack frame to find the first relevant code path.
  3. Identify the exception type and inner exception message.
  4. If it's an HTTP call, extract StatusCode and Body.
  5. Log a structured summary (message, type, HResult, command, stack top).
  6. Redact sensitive fields before persisting logs.
  7. Act on the actionable fields (e.g., bad config key, missing file, 401/403, timeouts).

Conclusion

Get-Error turns vague errors into actionable fixes. Expand inner exceptions, grab the exact command and line, and emit structured logs you can search and alert on. The result: faster debugging, clearer root causes, and fewer blind spots in production.

Want more PowerShell patterns like this? Check out the PowerShell Advanced Cookbook for proven recipes and deeper dives: PowerShell Advanced Cookbook.

← All Posts Home →