You wrote return inside a loop to stop at the first match. The loop kept going. Or you wrote break to skip the rest of one inner loop, and your whole outer loop quietly ended.
Both bugs come from the same mix-up: ForEach-Object and the foreach statement look alike, but they are different things. One is a cmdlet that runs a script block per pipeline item. The other is a language loop. return, break, and continue behave differently in each.
Everything below was run in PowerShell 7.4.6. The same rules apply in Windows PowerShell 5.1.
The shortest honest comparison
ForEach-Object (cmdlet) | foreach (...) { } (statement) | |
|---|---|---|
| Input | Pipeline, one item at a time | A collection already in memory |
| Current item | $_ / $PSItem | Your own variable ($file, $n) |
return | Skips to the next item (like continue) | Leaves the function or script |
break | Looks for an enclosing loop outside the pipeline | Exits this loop |
| Pipe the result onward | Works directly | Needs $( ) or a variable first |
| Speed on big in-memory lists | Slower (per-item script block call) | Faster |
One extra trap: when foreach appears after a pipe, it is not the statement. It is an alias:
(Get-Alias foreach).Definition # ForEach-Object So Get-ChildItem | foreach { ... } is ForEach-Object, with all of its rules. Microsoft Learn's about_Foreach covers the statement, and ForEach-Object covers the cmdlet.
Why return doesn't stop your loop
1..5 | ForEach-Object { if ($_ -eq 3) { return } "item $_" } item 1 item 2 item 4 item 5 The return only left the script block for item 3. ForEach-Object then called the same script block for item 4. In practice return inside ForEach-Object acts like continue in a normal loop (about_Return).
The foreach statement does what most people expected:
function Test-Foreach { foreach ($n in 1..5) { if ($n -eq 3) { return } "item $n" } 'after loop' } Test-Foreach item 1 item 2 Here return left the whole function, so even 'after loop' never ran. If you only want to skip one item, use continue:
foreach ($n in 1..5) { if ($n -eq 3) { continue } "item $n" } # item 1, item 2, item 4, item 5 Why break kills your outer loop
This one is nastier, because nothing errors:
foreach ($outer in 'A', 'B') { 1..3 | ForEach-Object { if ($_ -eq 2) { break } "$outer$_" } } 'after outer' A1 after outer B never ran. The ForEach-Object script block is not a loop, so break went looking for the nearest enclosing loop and found the outer foreach. Microsoft Learn's about_Break warns about this directly: break inside a pipeline such as a ForEach-Object script block can exit far more than the pipeline. With no enclosing loop at all, it can stop the whole script.
If you need to stop early, pick a tool built for it:
# Stop the pipeline after the first N results 1..1000000 | ForEach-Object { $_ } | Select-Object -First 3 # 1, 2, 3 # Or use statements and a labeled break :outer foreach ($a in 1..3) { foreach ($b in 1..3) { if ($b -eq 2) { break outer } "$a-$b" } } # 1-1 Select-Object -First stops the upstream pipeline once it has enough items. Labels are explained in about_Break and about_Continue.
Piping and speed
The foreach statement can't sit at the start of a pipeline:
foreach ($n in 1..3) { $n } | Sort-Object -Descending # An empty pipe element is not allowed. Wrap it in $( ), or assign it first:
$(foreach ($n in 1..3) { $n }) | Sort-Object -Descending # 3, 2, 1 $doubled = foreach ($n in 1..3) { $n * 2 } $doubled -join ',' # 2,4,6 On speed, I summed 200,000 numbers both ways on the same box, three runs each. The foreach statement took about 150 to 300 ms. ForEach-Object took about 740 to 900 ms. Your numbers will differ, but the gap is normal: the cmdlet invokes a script block for every item.
That doesn't make ForEach-Object the wrong choice. It streams, so it starts working on the first file while Get-ChildItem is still finding the rest, and it never needs the whole list in memory. It also has -Begin and -End blocks for setup and totals:
1..4 | ForEach-Object -Begin { $t = 0 } -Process { $t += $_ } -End { "total $t" } # total 10 Quick checklist
- Data already in a variable, and you want speed or real
break/return: use theforeachstatement. - Streaming a big pipeline (files, log lines, remote results): use
ForEach-Object. - Want to skip one item inside
ForEach-Object:returnworks, and that's the only thing it does there. - Want to stop a pipeline early:
Select-Object -First, notbreak. - Seeing
| foreach {in a script: that'sForEach-Object, not the statement. - Need to pipe a
foreachstatement's output: wrap it in$( )or assign it first.
Practice loops in a real terminal
Reading about loop control is quick; spotting it inside someone else's script is the hard part. CMD Master's free interactive PowerShell, CMD, and Bash terminal in the browser lets you run these loops with no install and see exactly where return and break land.
Have you ever had a break inside ForEach-Object end more of your script than you meant it to?