You redirected a script to a log file, opened the log, and got… nothing. The console showed every status line while the script ran. The file was empty. Or you called a helper function, assigned the result, and $result was $null even though the function clearly "printed" a value.
That is not a broken redirect. That is Write-Host.
Write-Host writes for the person watching the console. Write-Output (and bare values) write objects into the success stream — the pipeline everyone else can capture, redirect, and log. Mix them up and your logs go quiet.
Everything below was run in PowerShell 7.4.6. The same stream rules apply in Windows PowerShell 5.1.
The shortest honest comparison
Write-Host |
Write-Output / bare value |
|
|---|---|---|
| Destination | Host UI (information stream) | Success stream / pipeline |
$x = … captures it? |
No — $x is $null
|
Yes |
> log.txt / Out-File
|
Empty file (host still prints) | File has the text |
| Best for | Progress, color, "still running…" | Data a caller or log must keep |
Microsoft Learn's Write-Host page says it plainly: primary purpose is host-display-only output. Write-Output sends objects to the success stream. about_Output_Streams maps stream 1 to Write-Output and stream 6 to Write-Information / Write-Host.
The empty-log bug
Here is the failure that starts most of these tickets:
& {
Write-Host 'Starting job...'
Write-Host 'Done.'
} > ./job.log
(Get-Item ./job.log).Length
# 0
Get-Content ./job.log -Raw
# (empty)
The console still printed both lines. The file is zero bytes. Plain > only redirects the success stream (see about_Redirection). Write-Host never put anything there.
Swap in Write-Output:
& {
Write-Output 'Starting job...'
Write-Output 'Done.'
} > ./job2.log
Get-Content ./job2.log
Starting job...
Done.
Same shape with Out-File and the pipeline:
Write-Host 'hi-host-of' | Out-File ./log-host-of.txt
(Get-Item ./log-host-of.txt).Length
# 0
Write-Output 'hi-out-of' | Out-File ./log-out-of.txt
Get-Content ./log-out-of.txt
# hi-out-of
Assignment and the pipeline
Capture makes the difference impossible to miss:
$x = Write-Host 'hi-host'
$null -eq $x
# True
$y = Write-Output 'hi-out'
$y
# hi-out
$z = 'hi-bare' # bare value = same success stream as Write-Output
$z
# hi-bare
Pipeline consumers see the same split:
(Write-Host 'hi' | Measure-Object).Count
# 0
(Write-Output 'hi' | Measure-Object).Count
# 1
Write-Host painted hi on the screen. The next cmdlet received nothing on the success stream.
Functions that "print" return nothing useful
This is the quiet version of the empty-log bug — it shows up inside modules and helpers:
function Show-Bad {
Write-Host 'Computing...'
Write-Host 42
}
function Get-Good {
Write-Host 'Computing status...' # fine: human status
Write-Output 42 # data for the caller
}
function Get-Best {
42 # bare value is enough
}
$a = Show-Bad
$null -eq $a
# True
$b = Get-Good
$b
# 42
$c = Get-Best
$c
# 42
Show-Bad looked busy. The caller got $null. PSScriptAnalyzer's AvoidUsingWriteHost rule exists for exactly this: unless the verb is Show, prefer Write-Output / implicit output for data and Write-Verbose for optional chatter.
When Write-Host is the right tool
Keep Write-Host when a human watching the console is the consumer — colored prompts, step banners, "still scanning…" lines you do not want in the data pipeline:
Write-Host 'Scanning C:\Logs...' -ForegroundColor Cyan
$files = Get-ChildItem C:\Logs -File
Write-Host "Found $($files.Count) files." -ForegroundColor Green
$files | Select-Object Name, Length | Export-Csv .\report.csv -NoTypeInformation
Status stayed on screen. The CSV only got file objects. That split is intentional.
Write-Information, stream 6, and a 7.x note
Since Windows PowerShell 5.0, Write-Host is a wrapper around Write-Information and lands on the information stream (stream 6). That is why you can capture it if you ask for stream 6 on purpose:
Write-Host 'hi-info' 6> ./log-info.txt
Get-Content ./log-info.txt
# hi-info
Two gotchas that trip people on 7.x:
- Default
$InformationPreferenceisSilentlyContinue.Write-Informationstays quiet unless you set the preference or pass-InformationAction Continue.Write-Hoststill displays anyway — Microsoft Learn notes that$InformationPreferencedoes not suppressWrite-Host, except-InformationAction Ignore. - Prefer Write-Verbose for optional diagnostics the user can turn on with
-Verbose. Prefer bare values /Write-Outputfor anything a caller must keep.
Quick checklist
- Building data a function, pipeline, log, or file must keep: bare value or
Write-Output. - Banner / progress / colored console-only status:
Write-Host(orWrite-Information/Write-Verbose). - Empty log after
> fileorOut-File, but the console looked fine: search the script forWrite-Host. - Function "printed" a number and the caller got
$null: same root cause — host UI is not the success stream. - Need host text in a file on purpose: redirect stream 6 (
6>), or stop usingWrite-Hostfor that data.
Practice the stream split hands-on
Reading about streams is easy; muscle memory under a broken log is harder. CMD Master's free interactive browser terminal for PowerShell, CMD, and Bash lets you run these capture and redirect experiments with no install, until empty logs stop surprising you.
What was the last script that looked busy on screen and wrote a zero-byte log?
Top comments (0)