Most broken PowerShell scripts I've debugged didn't fail on some clever logic. They failed on a path glued together with a string: "$dir\$file". One trailing backslash, one empty variable, or one run on Linux, and the script writes to the wrong place or nowhere at all.
The Join-Path cmdlet fixes that. It combines a parent path and a child path with exactly one separator, and it fails loudly when the input is wrong instead of quietly guessing.
For the official reference, see Microsoft Learn's Join-Path and Split-Path docs.
The basic join
Join-Path -Path 'C:\Apps' -ChildPath 'logs'
# C:\Apps\logs
Positional works too, and it's how most people write it:
Join-Path C:\Apps logs
It cleans up your slashes
This is the main reason to use it. Whether the parent ends with a separator or the child starts with one, you get a single backslash:
Join-Path 'C:\Apps\' 'logs' # C:\Apps\logs
Join-Path 'C:\Apps' '\logs' # C:\Apps\logs
Join-Path 'C:\Apps\' '\logs' # C:\Apps\logs
String gluing doesn't do that. "C:\Apps\" + "\logs" gives you C:\Apps\\logs, which Windows usually tolerates, until that string ends up in a comparison, a log line, a registry value, or a tool that's pickier about it.
The empty-variable bug it saves you from
Here's the one that actually bites in production. Say $logDir comes from a config file and the key is missing:
$logDir = $config.LogDir # $null, nobody noticed
$target = "$logDir\app.log" # \app.log
Add-Content -Path $target -Value 'started'
\app.log is the root of the current drive. Your script happily writes C:\app.log (or fails with access denied, depending on who runs it), and you spend an hour wondering where the log went.
With Join-Path, the same mistake stops the script on the spot:
$target = Join-Path -Path $logDir -ChildPath 'app.log'
# Cannot bind argument to parameter 'Path' because it is null.
A clear error at the line that's wrong beats a file in the wrong place every time.
Join more than two parts
On PowerShell 7 (and 6), you can pass as many child parts as you want. Everything after the first child goes into -AdditionalChildPath:
Join-Path C:\Apps MyService logs 2026 app.log
# C:\Apps\MyService\logs\2026\app.log
Windows PowerShell 5.1 only takes one child, so the same line fails there with "A positional parameter cannot be found". Two options that work everywhere:
# Nest the calls
Join-Path (Join-Path (Join-Path C:\Apps MyService) logs) app.log
# Or put the separators in the child yourself
Join-Path C:\Apps 'MyService\logs\app.log'
The second one is fine. Join-Path still handles the seam between parent and child, which is where the bugs live.
Build the same child under many parents
-Path accepts an array, so you get one result per parent:
Join-Path -Path 'C:\Sites\shop', 'C:\Sites\blog', 'C:\Sites\api' -ChildPath 'web.config'
# C:\Sites\shop\web.config
# C:\Sites\blog\web.config
# C:\Sites\api\web.config
That pipes straight into the next step, for example to check which sites are missing a config. If you haven't seen it yet, my Test-Path guide covers that half:
Join-Path -Path 'C:\Sites\shop', 'C:\Sites\blog', 'C:\Sites\api' -ChildPath 'web.config' |
Where-Object { -not (Test-Path -Path $_ -PathType Leaf) }
-Resolve: join and confirm it exists
Add -Resolve and Join-Path returns the full path only if it actually exists. If it doesn't, you get an error instead of a string pointing at nothing:
Join-Path -Path $env:windir -ChildPath 'System32\drivers\etc\hosts' -Resolve
# C:\WINDOWS\System32\drivers\etc\hosts
-Resolve also expands wildcards, so it doubles as a quick finder:
Join-Path -Path C:\Apps\MyService\logs -ChildPath '*.log' -Resolve
You get every matching log as a full path. For anything fancier (recursion, filtering by date), reach for Get-ChildItem.
The drive gotcha
Join-Path checks that the drive exists, even without -Resolve:
Join-Path Q:\backup nightly.zip
# Cannot find drive. A drive with the name 'Q' does not exist.
It doesn't check folders or files, only the drive. That's usually helpful (a typo'd drive letter fails early), but it can surprise you when a script builds paths for a mapped drive that only exists on the target machine. In that case, build the string on the machine where the drive is mapped, or use a UNC path.
Why not [System.IO.Path]::Combine?
You'll see .NET's Combine in a lot of scripts. It mostly works, but it has a nasty rule: if the second part looks rooted, the first part is thrown away.
[System.IO.Path]::Combine('C:\Apps', '\logs')
# \logs
No error, no warning, and your path now points at the root of the current drive. Join-Path gives C:\Apps\logs for the same input. Inside PowerShell scripts, I'd stick with the cmdlet.
Scripts that work from any folder
The most useful habit of all: build paths from the script's own location, not from wherever someone happened to cd before running it.
$configPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json'
$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json
$PSScriptRoot is the folder the script lives in. Pair it with Join-Path and the script finds its files whether it's run from a scheduled task, a different drive, or a colleague's terminal.
Cross-platform for free
On PowerShell 7 for Linux and macOS, Join-Path uses / as the separator:
Join-Path -Path $HOME -ChildPath 'logs'
# /home/you/logs
PowerShell's own cmdlets often forgive a hardcoded "$HOME\logs" on Linux, but the moment that string goes to a native tool, a config file, or a .NET method, the backslash is just a character in a file name. If a script might ever run in a container or CI runner, letting Join-Path pick the separator is worth the switch.
Practical ops example: a dated log next to the script
$logDir = Join-Path -Path $PSScriptRoot -ChildPath 'logs'
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
$logFile = Join-Path -Path $logDir -ChildPath ('run-{0:yyyy-MM-dd}.log' -f (Get-Date))
Add-Content -Path $logFile -Value "$(Get-Date -Format s) job started"
Every run appends to one file per day, in a folder next to the script, no matter where it's launched from.
The other direction: Split-Path
When you need to take a path apart instead, Split-Path is the mirror image:
$p = 'C:\Apps\MyService\logs\app.log'
Split-Path -Path $p -Parent # C:\Apps\MyService\logs
Split-Path -Path $p -Leaf # app.log
On PowerShell 7 there's also -LeafBase (app) and -Extension (.log).
Quick checklist
- Combining any two path pieces? Use
Join-Path, not"$a\$b". - Worried about trailing or leading slashes?
Join-Pathcollapses them to one. - Variable might be empty?
Join-Paththrows instead of writing to the drive root. - More than two parts? Pass them all on PowerShell 7, or nest calls on 5.1.
- Need the path to really exist? Add
-Resolve. - Script needs its own files? Start from
$PSScriptRoot. - Avoid
[System.IO.Path]::Combinewhen the second part can start with\.
Practice it live
CMD Master is a free online interactive learning platform for the command line. You can practice PowerShell file and path commands in a live browser terminal with instant feedback, no install or VM needed. It also covers Windows CMD and Bash, most lessons are free, and premium unlocks the deeper paths. If you're just getting comfortable with the shell, start with the free interactive command line lessons on CMD Master.
Top comments (0)