PowerShell Functions: How to Write Reusable Code Blocks

The moment you catch yourself copying and pasting the same 10 lines of code into different parts of a script, it’s time to write a function. PowerShell functions wrap reusable logic into a named block you can call with different inputs and get consistent outputs. This guide covers basic function syntax, parameters with default values, return behavior, advanced functions with CmdletBinding, and documenting your functions with comment-based help.
Basic Function Syntax
Define a function with the function keyword, a name, and a script block:
function Get-Greeting {
Write-Output 'Hello from a function!'
}
# Call it
Get-Greeting
Hello from a function!
Function names follow the same Verb-Noun convention as built-in cmdlets. This makes your functions discoverable and consistent. Use approved verbs (run Get-Verb for the full list) to avoid warnings when including functions in modules.
Adding Parameters
Parameters let callers pass values into the function. Declare them in a param() block:
function Send-Report {
param(
[string] $Recipient,
[string] $Subject,
[string] $Body
)
Write-Output "To: $Recipient"
Write-Output "Subject: $Subject"
Write-Output "Body: $Body"
}
# Call with named parameters
Send-Report -Recipient '[email protected]' -Subject 'Daily Report' -Body 'All systems normal.'
# Call with positional parameters (order matches param order)
Send-Report '[email protected]' 'Daily Report' 'All systems normal.'
To: [email protected]
Subject: Daily Report
Body: All systems normal.
Type declarations like [string] are optional but recommended — they validate input and provide tab completion. If someone passes the wrong type, PowerShell throws a clear error message before the function body runs.
Parameter Default Values
Assign defaults by placing = value after the parameter declaration:
function Test-Connection-Custom {
param(
[string] $HostName,
[int] $Port = 80,
[int] $Timeout = 5
)
Write-Output "Testing $HostName on port $Port (timeout: ${Timeout}s)"
}
# Use all defaults except HostName
Test-Connection-Custom -HostName 'api.example.com'
# Override the default port
Test-Connection-Custom -HostName 'db.example.com' -Port 5432
Testing api.example.com on port 80 (timeout: 5s)
Testing db.example.com on port 5432 (timeout: 5s)
Default values are evaluated when the function is called, not when it’s defined. This means you can use Get-Date as a default — it returns the current time each call, not the time when the function was written.
Returning Values
PowerShell functions return output in two ways. Understanding both prevents a common source of bugs:
function Get-DiskUsagePercent {
param([string] $Drive = 'C:')
$disk = Get-PSDrive $Drive
$used = $disk.Used
$free = $disk.Free
$total = $used + $free
# Any output from the function body is returned
[math]::Round(($used / $total) * 100, 1)
}
$usage = Get-DiskUsagePercent -Drive 'C'
Write-Output "C: drive is $usage% full"
# Explicit return exits early
function Find-FirstAdmin {
param([string[]] $Users)
foreach ($user in $Users) {
if ($user -like '*admin*') {
return $user # Return this value and stop
}
}
return $null
}
C: drive is 67.4% full
In PowerShell, any uncaptured output inside a function is automatically part of the function’s return value — you don’t need return. Use return only to exit early. Use Write-Output or implicit output for actual return values, and Write-Host sparingly for console display that shouldn’t be captured.
Advanced Function with CmdletBinding
Adding [CmdletBinding()] above the param() block upgrades your function to a “cmdlet-like” advanced function with support for -Verbose, -WhatIf, and -ErrorAction:
function Remove-OldLogs {
[CmdletBinding(SupportsShouldProcess)]
param(
[string] $Path = 'C:\Logs',
[int] $DaysOld = 30
)
$cutoff = (Get-Date).AddDays(-$DaysOld)
$oldFiles = Get-ChildItem -Path $Path -Filter '*.log' |
Where-Object { $_.LastWriteTime -lt $cutoff }
foreach ($file in $oldFiles) {
if ($PSCmdlet.ShouldProcess($file.FullName, 'Delete')) {
Remove-Item $file.FullName
Write-Verbose "Deleted: $($file.Name)"
}
}
}
# Preview with -WhatIf
Remove-OldLogs -Path 'C:\Logs' -DaysOld 14 -WhatIf
# Run verbosely
Remove-OldLogs -Path 'C:\Logs' -DaysOld 14 -Verbose
The SupportsShouldProcess attribute enables -WhatIf and -Confirm parameters automatically. Write-Verbose output only appears when -Verbose is passed or $VerbosePreference is set to Continue.
Documenting with Comment-Based Help
Add comment-based help directly above the function to make it discoverable with Get-Help:
function Get-SystemSummary {
<#
.SYNOPSIS
Returns a brief summary of system resources.
.DESCRIPTION
Gets CPU load, available memory, and disk usage for the local machine.
.PARAMETER ComputerName
The computer to query. Defaults to localhost.
.EXAMPLE
Get-SystemSummary
Returns resource summary for the local machine.
.EXAMPLE
Get-SystemSummary -ComputerName 'server01'
Returns resource summary for server01.
#>
param(
[string] $ComputerName = $env:COMPUTERNAME
)
Write-Output "Summary for: $ComputerName"
}
# Now Get-Help works
Get-Help Get-SystemSummary -Examples
Common Errors and Fixes
-
Return statement vs implicit output — both work differently:
return 42returns 42 and exits the function. But any other expression that produces output (likeWrite-Output "debug") also becomes part of the function’s output. Unintentional output from inner cmdlets gets mixed into the return value. Use| Out-Nullor$null = ...to suppress output you don’t want returned. -
Variable scope: function can’t see outer variables by default: Variables defined outside the function are not directly accessible inside (unless they’re in the global scope or you use the
$script:scope prefix). The correct approach is to pass values as parameters — it makes the function self-contained and testable.
Related Cmdlets / See Also
Wrapping Up
Functions are the cornerstone of maintainable PowerShell scripts. Start with basic param() blocks, add type hints and defaults, and upgrade to [CmdletBinding()] when you need -WhatIf or -Verbose support. Document with comment-based help from day one — your future self will thank you. Your next step: identify the most-repeated code block in your current scripts and wrap it in a named function.


