PowerShell error handling is great but becomes more complicated as your functions become more complicated and interdependent. For a while I used -ErrorAction Stop on everything because I was available to immediately deal with unforseen problems, but when functions call functions sometimes you don't really want them to just stop, you want to continue or possibly parse the output later.

Good solutions are definitely possible but here are some ground-rules to help make things clear:

  • Write-Host cannot be captured, it will always print to the console. So don't use it for anything you'll ever want to capture or inspect later.
  • Write-Output can be captured and processed. Use this instead of Write-Host where needed.
  • Write-Error -ErrorAction Continue can be captured if you use &2>1 when calling the function. This redirects the error stream. Write-Debug, Write-Warning, and Write-Verbose can be captured in a similar way in PowerShell 3.0+. Check:

    Get-Help about_Redirection
    
  • Or you can trap both -ErrorAction Continue and -ErrorAction Stop by wrapping the call in a try block. This will also allow you to trap and parse unexpected exceptions. If you don't use a try block and do an -ErrorAction Stop it will look like nothing prior to the Error gets saved into any caller variable; but it does it's just all bundled into the exception which is why you have to catch it.

Time for the evidence. If you tried to capture output normally, only Write-Output is captured:

function Tester {
	param (
	)

	Write-Host "Write-Host is console only."
	Write-Output "Write-Output can be captured."
	Write-Error "I made an Error."
}

$ErrorActionPreference = "Continue"
$result = $null
$result = Tester
"Function success: $?"
"Result is `$null: $(if ($result -eq $null) {"Yes"} else {"No"})"
"Result is: $result"
Write-Host is console only.
Tester : I made an Error.
At line:8 char:11
+ $result = Tester
+           ~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Tester
Function success: True
Result is $null: Yes
Result is: Write-Output can be captured.

And if you tried to capture the Error:

$result = $null
$result = Tester 2>&1
"Function success: $?"
"Result is `$null: $(if ($result -eq $null) {"Yes"} else {"No"})"
"Result is: $result"
Write-Host is console only.
Function success: True
Result is $null: No.
Result is: Write-Output can be captured. Error.

But neither of the above will work with an ErrorAction of Stop:

$ErrorActionPreference = "Stop"
$result = $null
$result = Tester # or Tester 2>&1
"Function success: $?"
"Result is `$null: $(if ($result -eq $null) {"Yes"} else {"No"})"
"Result is: $result"
Error.
At :line:2 char:11
+ $result = T <<<< ester

Function success: False
Result is $null: Yes
Result is:

Which is why you have to put it in a try block:

$ErrorActionPreference = "Stop"
$result = $null
$result = try { Tester 2>&1 } catch { $_ }
"Function success: $?"
"Result is `$null: $(if ($result -eq $null) {"Yes"} else {"No"})"
"Result is: $result"
Write-Host is console only.
Function success: True
Result is $null: No
Result is: Write-Output can be captured. Error.

But note that the try block won't catch an error by itself unless it's a Stop error. So you'll need to use 2>&1 (to catch Continue errors) and the try block together.

Now you have a $result though, how do you determine if an error occurred - especially when the above shows the status flag $? isn't set to True when ErrorAction is set to Continue? It's easy because $result is actually an array of strings and ErrorRecord objects (including any exceptions or errors caught in the catch block). You can use this to your advantage to filter out information (or errors) and deal with them separately.

if ($result | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }) {
    # There was an error.
}

There is one other tiny issue which will crop up. In the above examples I print $result as part of a string, and so it gets converted to a string:

"Result is: $result"
# But if I had done this...
$result
# It would show this...
Result is: Write-Output can be captured. Error.
Write-Output can be captured.
Tester : Error.
At line:3 char:11
+ $result = Tester 2>&1
+           ~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Tester

When a Write-Error is redirected, and then examined in this way, it will print to the console in red. When it is caught in the catch block, it won't show in red. It's really minor but the reason for that is a property flag doesn't get set. You can fix that with this:

$result | %{ if ($_ -is [System.Management.Automation.ErrorRecord] -and (!$_.writeErrorStream)) { $_ | Add-Member NoteProperty writeErrorStream $True } }
$result