PowerShell ForEach-Object vs foreach Statement: Full Comparison

PowerShell ForEach-Object vs foreach Statement: Full Comparison

PowerShell Tips Editor 1 min read
PowerShell ForEach-Object vs foreach Statement: Full Comparison

They both iterate over a collection, they both use $_ to reference the current item, and beginners use them interchangeably — until one approach slows a script to a crawl or breaks a pipeline. PowerShell foreach vs ForEach-Object have fundamentally different behaviors: the foreach keyword loads the entire collection into memory at once, while ForEach-Object processes items one at a time as they stream through the pipeline. Choosing the right one depends on whether you’re in a pipeline and how much data you’re handling.

Syntax Comparison

Both loop over collections, but their positions in code are different. The foreach statement is a language construct; ForEach-Object is a cmdlet that sits in a pipeline.

# foreach statement — language construct
$servers = @("server01", "server02", "server03")
foreach ($server in $servers) {
    Write-Output "Processing: $server"
}

# ForEach-Object — pipeline cmdlet (alias: %)
$servers | ForEach-Object {
    Write-Output "Processing: $_"
}

# Both produce identical output in this simple case
# Assign results from foreach (collect all iterations)
$results = foreach ($s in $servers) {
    [PSCustomObject]@{ Server = $s; Reachable = (Test-Connection $s -Count 1 -Quiet) }
}

# Assign results from ForEach-Object (also works)
$results = $servers | ForEach-Object {
    [PSCustomObject]@{ Server = $_; Reachable = (Test-Connection $_ -Count 1 -Quiet) }
}

Pipeline Position Difference

This is the defining difference. ForEach-Object is a cmdlet and can sit anywhere in a pipeline. The foreach keyword is a statement — it cannot receive pipeline input or be mid-pipeline.

# ForEach-Object in the middle of a pipeline — works correctly
Get-ChildItem -Path "C:\Logs" -Filter "*.log" |
    ForEach-Object { "$($_.Name) — $([math]::Round($_.Length/1KB,1)) KB" } |
    Where-Object { $_ -like "*error*" }

# foreach keyword CANNOT be in the middle of a pipeline
# Get-ChildItem | foreach ($f in ??) { }  -- this is invalid

If you need to iterate over pipeline results mid-stream, you must use ForEach-Object.

Memory Use: foreach Loads All, ForEach-Object Streams

The foreach statement requires the entire collection to be in memory before the first iteration begins. ForEach-Object processes one object at a time as it arrives from the pipeline — memory usage stays constant regardless of input size.

# foreach — entire file loaded into memory first
$lines = Get-Content -Path "C:\Logs\bigfile.log"  # Loads ALL lines
foreach ($line in $lines) {
    if ($line -match "ERROR") { Write-Output $line }
}

# ForEach-Object — one line at a time, constant memory
Get-Content -Path "C:\Logs\bigfile.log" |
    ForEach-Object {
        if ($_ -match "ERROR") { Write-Output $_ }
    }

# Even simpler (no ForEach-Object needed here — just Where-Object)
Get-Content -Path "C:\Logs\bigfile.log" | Where-Object { $_ -match "ERROR" }

For files larger than a few hundred MB, the streaming approach of ForEach-Object (or Where-Object) can prevent out-of-memory conditions.

Performance Benchmarks

For small collections, foreach is typically faster because it avoids pipeline overhead. For large streaming scenarios, ForEach-Object wins on memory. Measure both when performance is critical.

# Simple benchmark comparison
$items = 1..10000

$time1 = Measure-Command {
    foreach ($i in $items) { $null = $i * 2 }
}

$time2 = Measure-Command {
    $items | ForEach-Object { $null = $_ * 2 }
}

Write-Output "foreach:         $($time1.TotalMilliseconds) ms"
Write-Output "ForEach-Object:  $($time2.TotalMilliseconds) ms"

Typical results show foreach is 3-5x faster for pure iteration of in-memory arrays, while ForEach-Object is preferred for streaming and pipeline composition.

Using $_ vs Named Variable

Inside ForEach-Object, the current item is $_ (or $PSItem). Inside a foreach statement, you name the variable yourself in the declaration.

"# foreach — named variable, clearer in complex blocks
foreach ($computer in $computerList) {
    $diskFree = (Get-PSDrive -Name C -PSProvider FileSystem).Free
    Write-Output "$computer has $([math]::Round($diskFree/1GB,1)) GB free"
}

# ForEach-Object — $_ can become confusing in nested scenarios
$computerList | ForEach-Object {
    $computer = $_  # Assign to named variable for clarity in nested code
    $diskFree = Invoke-Command -ComputerName $computer -ScriptBlock { (Get-PSDrive C).Free }
    Write-Output "$computer has $([math]::Round($diskFree/1GB,1)) GB free"
}

When to Use Which

Decision guide:

  • Use foreach when: iterating an in-memory array, performance matters most, not in a pipeline, named variables make the code clearer
  • Use ForEach-Object when: receiving pipeline input, processing large files or streams, mid-pipeline position required, writing one-liners

Common Errors and Fixes

  • foreach cannot be mid-pipeline: Writing Get-Process | foreach ($p in ??) { } fails because foreach is a statement, not a cmdlet — it has no pipeline binding. The fix is simple: replace with Get-Process | ForEach-Object { }. In scripts where you’ve already assigned the collection to a variable, the foreach statement is perfectly appropriate.
  • Breaking out of ForEach-Object uses return not break: Inside ForEach-Object, break exits the entire pipeline (including cmdlets before it), which is almost never what you want. Use return to skip to the next item (equivalent to continue in a traditional loop). To exit early from ForEach-Object, the only clean approach is to filter with Where-Object before the ForEach-Object block.

Related Cmdlets / See Also

Wrapping Up

For script clarity and moderate data sizes, foreach with a named variable wins on readability. For pipelines, streaming large data, or one-liners, ForEach-Object is the right tool. As a next step, profile a loop in one of your existing scripts with Measure-Command using both approaches — the results will help you internalize when each method performs better in your specific workload.

Send-Item -To