PowerShell Foreach Loop: Complete Tutorial with Examples

You have a list of 500 servers, and you need to restart a service on each one. You have 200 log files and need to check each for a specific error. You have a CSV of user accounts to create in Active Directory. All of these problems have the same solution: a PowerShell foreach loop. It’s the most-used iteration construct in PowerShell scripting, and this guide covers every form of it — the foreach statement, ForEach-Object in the pipeline, break and continue, nesting, and a real bulk file processing example.
Basic foreach Statement Syntax
The foreach statement iterates over a collection, assigning each element to a named variable in turn:
$servers = @('web01', 'web02', 'db01', 'cache01')
foreach ($server in $servers) {
Write-Output "Processing: $server"
}
# foreach works on any enumerable — arrays, file lists, ranges
foreach ($n in 1..5) {
Write-Output "Number: $n"
}
Processing: web01
Processing: web02
Processing: db01
Processing: cache01
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5
The loop variable ($server) is yours to name. It’s scoped to the loop but remains accessible after the loop exits — it holds the last value from the iteration. Modify a copy of the collection element if you need the original unchanged.
ForEach-Object in the Pipeline
ForEach-Object is the pipeline cmdlet version. It processes each object that flows through the pipeline, using $_ to reference the current object:
# Pipeline version — data streams from Get-ChildItem
Get-ChildItem C:\Logs -Filter '*.log' | ForEach-Object {
$size = [math]::Round($_.Length / 1KB, 2)
Write-Output "$($_.Name): $size KB"
}
# Short alias % does the same thing
Get-Service | Where-Object { $_.Status -eq 'Running' } | % {
$_.Name
}
app.log: 12.50 KB
error.log: 4.82 KB
Inside ForEach-Object, always use $_ to reference the current pipeline object. Forgetting $_ and using the variable name from an outer loop is one of the most common mistakes.
foreach vs ForEach-Object: When to Use Which
Both loop through collections, but they have important differences:
- foreach statement — Faster for in-memory collections. Loads the entire collection before iterating. Supports
breakandcontinuenatively. - ForEach-Object — Slower per item due to pipeline overhead. Streams data, so it works with cmdlets that produce output progressively (like reading from a database or large file). Lower peak memory usage.
- ForEach-Object with -Parallel (PowerShell 7+) — Runs iterations in parallel threads, dramatically faster for I/O-bound operations.
# For large in-memory work: foreach statement is ~3x faster
$items = 1..10000
foreach ($item in $items) { $item * 2 }
# For streaming pipeline data: ForEach-Object
Get-ChildItem C:\Logs -Recurse | ForEach-Object { $_.FullName }
Using Break and Continue
break exits the loop entirely; continue skips to the next iteration:
$servers = @('web01', 'web02', 'MAINTENANCE', 'db01')
foreach ($server in $servers) {
if ($server -eq 'MAINTENANCE') {
Write-Warning "Stopping at maintenance server"
break # Exit loop
}
Write-Output "Pinging: $server"
}
# continue: skip this iteration, proceed to next
foreach ($n in 1..10) {
if ($n % 2 -eq 0) { continue } # Skip even numbers
Write-Output $n
}
Pinging: web01
Pinging: web02
WARNING: Stopping at maintenance server
1
3
5
7
9
Nested Foreach Loops
Loops inside loops handle multi-dimensional data or combinations:
$environments = @('DEV', 'STAGING', 'PROD')
$services = @('API', 'Worker', 'Scheduler')
foreach ($env in $environments) {
foreach ($svc in $services) {
Write-Output "$env-$svc"
}
}
DEV-API
DEV-Worker
DEV-Scheduler
STAGING-API
STAGING-Worker
STAGING-Scheduler
PROD-API
PROD-Worker
PROD-Scheduler
In nested loops, break exits only the innermost loop. To break out of multiple levels, use a flag variable or restructure into a function that uses return.
Real Example: Bulk Process Files
Rename all .log files in a folder to include today’s date prefix:
$sourceDir = 'C:\Logs\Archive'
$today = Get-Date -Format 'yyyy-MM-dd'
Get-ChildItem -Path $sourceDir -Filter '*.log' | ForEach-Object {
$newName = "${today}_$($_.Name)"
$newPath = Join-Path $_.DirectoryName $newName
if (-not (Test-Path $newPath)) {
Rename-Item -Path $_.FullName -NewName $newName
Write-Output "Renamed: $($_.Name) -> $newName"
} else {
Write-Warning "Skipped: $newName already exists"
}
}
Renamed: app.log -> 2026-05-04_app.log
Renamed: error.log -> 2026-05-04_error.log
Common Errors and Fixes
-
Modifying collection while iterating causes errors: Removing items from an array while looping over it with
foreachthrows an error. Collect the items to remove in a separate array, then remove them after the loop completes. -
Forgetting $_ inside ForEach-Object: Inside
ForEach-Object { }, the current item is$_— not the name you might use in aforeach ($item in $collection)statement. Using the wrong variable returns$nullsilently.
Related Cmdlets / See Also
Wrapping Up
The foreach statement is the workhorse of PowerShell iteration — fast, readable, and flexible. Use it for in-memory collections and the ForEach-Object cmdlet when processing pipeline data. Remember that $_ is the current object in pipeline contexts. Your next step: take a manual, repetitive task you do every week and build a foreach loop that automates it.


