Concise, Predictable Flow with && and || in PowerShell 7
You want follow-on steps to run only when the previous one truly succeeded, and you want clean fallbacks when they didnt. PowerShell 7introduces pipeline chain operators && and || that make this pattern concise, readable, and reliable without noisy if blocks. In this post, youll learn how they work, how success and failure are determined, and practical patterns to chain PowerShell cmdlets and native tools.
How && and || work in PowerShell 7
Success vs. failure semantics
The pipeline chain operators run the right-hand pipeline based on the left-hand pipelines outcome:
left && right: Run right only if left succeeded.left || right: Run right only if left failed.
What counts as success or failure?
- PowerShell cmdlets: If any error is written (terminating or non-terminating), the pipeline is considered a failure. Suppressing an error with
-ErrorAction SilentlyContinuestill marks the pipeline as failed (but hides the message). - Boolean-producing commands: If the final result is a single boolean,
$truemeans success,$falsemeans failure. This makes guards likeTest-Pathwork as you expect. - Native executables: Exit code
0is success; non-zero is failure. PowerShell uses$LASTEXITCODEunder the covers.
Version requirement and basic syntax
- These operators require PowerShell 7+ (not available in Windows PowerShell 5.1).
- You can break lines after
&&or||for readability. - Use parentheses to group multiple commands on either side.
# Ensure we're on PowerShell 7+
if ($PSVersionTable.PSVersion.Major -lt 7) { throw 'This script requires PowerShell 7 or later.' }
# Anatomy
first-command && second-command
first-command || fallback-command
# Grouped pipelines
(first; second) && (third; fourth)
Practical patterns you can use today
1) Guard then do (fast path on success)
Use a check that returns a boolean, and only continue when its true.
# If the data file exists, read and inspect its first two items
Test-Path './data.json' &&
(Get-Content './data.json' -Raw |
ConvertFrom-Json |
Select-Object -First 2 |
Format-List)
Because Test-Path returns $true/$false, the right-hand pipeline runs only when the file exists. This pattern replaces an if (Test-Path) { ... } block with a single expressive chain.
2) Fallback cleanly on failure
When a precondition or operation fails, do something else create a default file, choose a backup endpoint, or warn the user.
# Create a default config only if it's missing
Test-Path './config.json' ||
([IO.File]::WriteAllText(
'./config.json',
'{ "Enabled": true }',
[Text.UTF8Encoding]::new($false)
))
# Try a primary API call; fall back to a cached copy if it fails
Invoke-RestMethod 'https://api.example.com/data' -ErrorAction SilentlyContinue ||
(Get-Content './cache/data.json' -Raw | ConvertFrom-Json)
Note the use of -ErrorAction SilentlyContinue to hide the error message while still marking the pipeline as failed so the fallback executes.
3) Mix native tools and PowerShell without if/else
Chain native commands with cmdlets, suppressing noisy stderr when you only care about the outcome.
# If we're inside a git repo, show the count of changed files; otherwise print a friendly note
& git rev-parse --is-inside-work-tree 2>$null &&
(& git status --porcelain |
Measure-Object -Line |
Select-Object -ExpandProperty Lines) ||
(Write-Host 'Not a git repo or git is not available.')
Here, a non-zero exit code from git rev-parse triggers the fallback. The 2>$null redirection hides the error text while preserving the failure state.
4) Group and assign: chain more than one step
You can group multiple commands and even capture results mid-chain.
# Parse JSON only if the file exists; then only if parsing succeeded, write a projection
Test-Path './data.json' &&
($json = Get-Content './data.json' -Raw | ConvertFrom-Json) &&
($json | Select-Object Name, Version | Format-Table)
# If a build step succeeds, publish; otherwise, archive logs
(Invoke-Build) && (Publish-Artifacts) || (Compress-Archive -Path .\logs -DestinationPath .\logs.zip)
If any step fails, subsequent &&-chained steps wont run, and the || branch kicks in.
5) Retry and soft dependencies
Use || to implement simple retry logic or fallbacks to secondary services.
# One retry: try, then try again with a short delay
(Invoke-RestMethod 'https://api.example.com/health' -TimeoutSec 3 -ErrorAction SilentlyContinue) ||
(Start-Sleep -Seconds 2; Invoke-RestMethod 'https://api.example.com/health' -TimeoutSec 5 -ErrorAction SilentlyContinue) ||
(Write-Warning 'Service unhealthy after retry.')
# Prefer primary module; fall back to a compatible alternative if import fails
Import-Module Primary.Analytics -ErrorAction SilentlyContinue ||
Import-Module Compat.Analytics
Make success predictable
Control error behavior
- Non-terminating errors still mark the pipeline as failed, which is perfect for
||. - If you wrap code in
try { ... } catch { ... }and catch the error, the pipeline looks successful unless you rethrow or emit a failure. To make the chain see a failure, either rethrow or write an error intentionally.
# Surface failure to the chain after handling details
try {
Invoke-Sqlcmd -InputFile .\migrate.sql -ErrorAction Stop
}
catch {
Write-Error "Migration failed: $($_.Exception.Message)" # keeps failure state
}
# Or rethrow to fail fast
try {
Invoke-WebRequest 'https://api.example.com' -ErrorAction Stop
}
catch {
throw # makes the left pipeline fail so the || branch runs
}
Silence noise without hiding outcomes
- PowerShell cmdlets:
-ErrorAction SilentlyContinuehides messages but still marks failure. - Native commands: redirect stderr with
2>$nullto hide text while keeping exit code semantics.
# Hide error text, still fail -> triggers fallback
(Get-Item .\missing.txt -ErrorAction SilentlyContinue) || (New-Item .\missing.txt -ItemType File)
# Native tool: hide stderr, still fail if exit code != 0
& tar -czf site.tgz dist 2>$null && (Write-Host 'Archive created.') || (Write-Warning 'tar failed; skipping packaging.')
Intentionally fail a step
Sometimes you want to halt a chain on a logical condition that isnt an error by default. Emit a failure explicitly.
# Treat empty results as a failure so || fallback runs
($result = Get-Content .\data.json -Raw | ConvertFrom-Json) | Out-Null
($result.Count -gt 0) || (Write-Error 'No items found in data.json')
# Validate prerequisites
(Test-Path .\env.ps1) && (. .\env.ps1) || (throw 'Missing env.ps1')
Avoid common pitfalls
- Windows PowerShell 5.1 doesnt support
&&/||. Use PowerShell 7+. - Formatting cmdlets like
Format-Table/Format-Listare for display; avoid piping their output to logic that expects objects. Use them at the end of a display-only chain. - If you rely on a boolean guard, ensure the last statement actually produces a single boolean. Many cmdlets return objects, not
$true/$false. - Remember that suppressed errors (
-ErrorAction SilentlyContinueor2>$null) still represent failure for chaining. Thats usually what you want for clean fallbacks.
End-to-end examples
# 1) Guard read + format
Test-Path './data.json' &&
(Get-Content './data.json' -Raw | ConvertFrom-Json | Select-Object -First 2 | Format-List)
# 2) Create default config only when missing
Test-Path './config.json' ||
([IO.File]::WriteAllText('./config.json', '{ "Enabled": true }', [Text.UTF8Encoding]::new($false)))
# 3) Native + PowerShell with clean fallback
& git rev-parse --is-inside-work-tree 2>$null &&
(& git status --porcelain | Measure-Object -Line | Select-Object -ExpandProperty Lines) ||
(Write-Host 'Not a git repo or git missing.')
What you get: less branching, cleaner scripts, safer fallbacks, and faster code reviews. Reach for && to continue on success and || to fall back on failure. Compose small, reliable steps and let the chain operators do the control flow for you.