- Courses
- 💻Programming Languages
- 🪟PowerShell
- Scripting Fundamentals
- Functions and Modular Code
- How to Define Functions
On this page
PowerShell Functions – From Basic to Advanced
Updated on June 11, 2026
By Enrico Hochkoeppler
Web Developer & TutoriaLibre Founder
A PowerShell function is a named, reusable block of code that performs a specific task. Functions are the primary tool for turning repetitive scripts into organised, maintainable, tool-oriented code. A basic function needs only a name and a script block. A production-quality function adds typed parameters, validation, pipeline support, verbose output, error handling, and built-in help — all using built-in PowerShell mechanisms that cost very little to add and pay back substantially in reliability and discoverability.
Basic Function Syntax
A function is declared with the function keyword followed by a name and a script block. The code inside the braces runs whenever the function is called:
function Get-Greeting {
"Hello from the function."
}
Get-Greeting
Get-Greeting is the function name. Calling it by that name executes everything inside the braces. Output: Hello from the function.
Function Naming Conventions
Naming functions correctly is not optional — it determines discoverability, import behaviour, and professionalism.
Use the Verb-Noun format. PowerShell uses approved verbs to create a consistent command surface. Every function name should be ApprovedVerb-Noun. List all approved verbs with:
Get-Verb | Sort-Object -Property Verb
Each verb belongs to a group — Get, Set, New, Remove are in the Common group; Start, Stop, Enable, Disable are in Lifecycle. Using a verb from this list ensures your function integrates naturally with the rest of PowerShell.
Use Pascal case. Get-ProcessReport, not get-processreport or getprocessreport.
Use a singular noun. Get-ComputerInfo, not Get-ComputersInfo.
Prefix the noun to avoid conflicts. Generic names like Get-Version or Get-Info will eventually conflict with another module or future built-in. Prefix your noun with initials or a project abbreviation:
# Too generic — risk of conflict
function Get-Version { ... }
# Better — prefix prevents collision
function Get-PSVersion { ... }
# Best — unique prefix reduces conflict to near zero
function Get-MrPSVersion { ... }
Using an unapproved verb causes PowerShell to emit a warning every time the function's module is imported, making it look unprofessional and limiting discoverability.
Parameters
Parameters make functions reusable by accepting different inputs on each call. Define them in a param() block at the top of the function body.
Use the same parameter names as built-in cmdlets wherever possible. $ComputerName not $Computer, $Server, or $Host. This consistency makes your functions feel native and helps users discover them:
function Get-ComputerStatus {
param(
$ComputerName
)
Write-Output $ComputerName
}
Get-ComputerStatus -ComputerName "Server01"
$ComputerName receives the string "Server01" and Write-Output sends it to the pipeline. Output: Server01.
Multiple parameters are separated by commas:
function Add-Numbers {
param(
[int]$A,
[int]$B
)
$A + $B
}
Add-Numbers -A 5 -B 7
Both $A and $B are typed as [int]. PowerShell converts compatible strings to integers automatically. If an incompatible value is provided, PowerShell throws a type conversion error before the function body runs. Output: 12.
Returning Values
PowerShell functions return everything they output — not just what follows a return statement. Any expression or cmdlet that writes to the pipeline from inside the function contributes to the output:
function Square {
param([int]$N)
$N * $N # implicit output — placed on the pipeline
}
Square -N 5 # Output: 25
The arithmetic result $N * $N is automatically written to the output pipeline. No return keyword is required.
The return keyword exits the function immediately and optionally emits one final value. It does not suppress any output produced before it:
function Test-Return {
"Line 1" # emitted
"Line 2" # emitted
return "Done" # "Done" is emitted, then function exits
"Line 4" # never reached
}
Test-Return
# Output: Line 1, Line 2, Done
Use return when you need to exit the function early — for example, after a validation check — not as the only mechanism for producing output.
Advanced Functions: Adding [CmdletBinding()]
Adding the [CmdletBinding()] attribute to a function transforms it from a basic function into an advanced function — one that behaves like a compiled cmdlet. This single addition unlocks the full set of PowerShell common parameters:
function Get-MrPSVersion {
[CmdletBinding()]
param(
$ComputerName
)
Write-Output $ComputerName
}
After adding [CmdletBinding()], the function automatically gains -Verbose, -Debug, -ErrorAction, -WarningAction, -ErrorVariable, -OutVariable, -OutBuffer, and more. Verify this:
Get-Command -Name Get-MrPSVersion -Syntax
# Get-MrPSVersion [[-ComputerName] <Object>] [<CommonParameters>]
(Get-Command -Name Get-MrPSVersion).Parameters.Keys
# ComputerName, Verbose, Debug, ErrorAction, WarningAction, ...
[CmdletBinding()] requires a param() block, but the block may be empty. A basic function without param() cannot use [CmdletBinding()].
SupportsShouldProcess — Adding -WhatIf and -Confirm
Any function that modifies state — deleting files, restarting services, changing registry values — should support -WhatIf and -Confirm. These are safety parameters that let callers test what a function would do before it does it. Add them by setting SupportsShouldProcess on the [CmdletBinding()] attribute:
function Remove-OldLogs {
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$Path
)
$files = Get-ChildItem -Path $Path -File
foreach ($file in $files) {
if ($PSCmdlet.ShouldProcess($file.FullName, "Remove file")) {
Remove-Item -Path $file.FullName
}
}
}
$PSCmdlet.ShouldProcess($target, $action) is the gate that checks whether the caller passed -WhatIf or -Confirm. When -WhatIf is active, ShouldProcess returns $false and prints what would have happened without executing anything. When -Confirm is active, it prompts the user interactively. This protects against accidental mass deletions and gives scripts natural dry-run capability.
Usage:
Remove-OldLogs -Path "C:\Logs" -WhatIf
# What if: Performing the operation "Remove file" on target "C:\Logs\app.log".
Parameter Validation
Validate input at the parameter level, not inside the function body. Catching invalid input before the function body runs produces cleaner error messages and prevents partial execution.
Mandatory — require a value
function Get-ServiceStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ServiceName
)
Get-Service -Name $ServiceName
}
[Parameter(Mandatory)] means the caller must provide a value for $ServiceName. If they omit it, PowerShell prompts interactively rather than proceeding with a null value. Output of Get-Service -Name $ServiceName is whatever the service status is.
Array parameters — accept multiple values
Add [] to the type to allow an array:
function Get-ServiceStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string[]]$ServiceName
)
foreach ($name in $ServiceName) {
Get-Service -Name $name
}
}
Get-ServiceStatus -ServiceName "bits", "wuauserv"
[string[]] accepts one or more strings. The foreach loop processes each service name individually.
Default values
function Get-ComputerUptime {
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
Write-Output $ComputerName
}
$env:COMPUTERNAME evaluates at call time to the name of the local machine. This is a dynamic default — far better than hardcoding a server name. [ValidateNotNullOrEmpty()] ensures the parameter cannot be passed as $null or an empty string, even by a caller who explicitly provides one.
Common validation attributes
function Set-LogLevel {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateSet("Debug", "Info", "Warning", "Error")]
[string]$Level,
[ValidateRange(1, 100)]
[int]$RetentionDays = 30,
[ValidatePattern("^[A-Z]{2,4}-\d{4}$")]
[string]$TicketId,
[ValidateScript({ Test-Path $_ })]
[string]$LogPath
)
Write-Output "Level: $Level | Days: $RetentionDays"
}
Each validation attribute does the following:
[ValidateSet()]— accepts only the listed values. PowerShell provides tab-completion for the valid options automatically.[ValidateRange(min, max)]— accepts only integers between the specified bounds.[ValidatePattern(regex)]— accepts only strings matching the regular expression.[ValidateScript({ })]— runs a script block; the parameter is valid only if the block returns$true. Here, the log path must exist on disk.
Verbose Output
Inline comments are invisible to the caller. Write-Verbose produces diagnostic messages that appear only when the caller passes -Verbose, keeping normal output clean while providing detailed tracing on demand:
function Invoke-DataExport {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ExportPath
)
Write-Verbose "Starting export to: $ExportPath"
# ... export logic ...
Write-Verbose "Export complete."
Write-Output "Exported successfully to $ExportPath"
}
# Normal call — no verbose messages
Invoke-DataExport -ExportPath "C:\Export"
# Verbose call — diagnostic messages visible
Invoke-DataExport -ExportPath "C:\Export" -Verbose
# VERBOSE: Starting export to: C:\Export
# VERBOSE: Export complete.
# Exported successfully to C:\Export
Write-Verbose requires [CmdletBinding()] to be present on the function. Without it, the -Verbose common parameter does not exist and the messages never appear.
Pipeline Input
A function that supports pipeline input integrates naturally with the rest of PowerShell. There are two pipeline binding mechanisms.
ValueFromPipeline — bind by object type
function Test-Connection {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
process {
foreach ($Computer in $ComputerName) {
Write-Output "Testing: $Computer"
}
}
}
"Server01", "Server02" | Test-Connection
ValueFromPipeline binds the entire piped object (when its type matches the parameter type) to $ComputerName. The process block runs once per piped item. The foreach handles the case where the piped value is an array. Output:
Testing: Server01
Testing: Server02
ValueFromPipelineByPropertyName — bind by property name
function Start-ComputerScan {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
process {
Write-Output "Scanning: $ComputerName"
}
}
Get-ADComputer -Filter * | Start-ComputerScan
ValueFromPipelineByPropertyName reads the value of a matching-named property from each piped object. Since Get-ADComputer emits objects with a ComputerName property, PowerShell binds it to the $ComputerName parameter automatically.
begin, process, and end
function Measure-Items {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[object]$InputObject
)
begin {
$count = 0
Write-Verbose "Starting count"
}
process {
$count++
Write-Verbose "Processing item: $InputObject"
}
end {
Write-Output "Total items: $count"
}
}
1..10 | Measure-Items
The begin block runs once before any pipeline input arrives — used for initialisation. The process block runs once for every object piped in. The end block runs once after all input is consumed — used for finalisation or aggregate output. Output: Total items: 10.
Error Handling with try/catch
By default, many cmdlets produce non-terminating errors that flow past a try/catch block. To catch them, add -ErrorAction Stop to the cmdlet call, which promotes the non-terminating error to a terminating one:
function Connect-RemoteComputer {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
process {
foreach ($Computer in $ComputerName) {
try {
Test-WSMan -ComputerName $Computer -ErrorAction Stop
Write-Output "$Computer : Connected"
}
catch {
Write-Warning "Unable to connect to: $Computer. Error: $($_.Exception.Message)"
}
}
}
}
"Server01", "MissingServer" | Connect-RemoteComputer
-ErrorAction Stop on Test-WSMan ensures any connection failure is raised as a terminating error, which the catch block can intercept. $_.Exception.Message retrieves the error message from the caught exception object. Without -ErrorAction Stop, the catch block would never execute for cmdlet errors.
Do not set the global
$ErrorActionPreference = 'Stop'as a workaround. Modifying the global preference affects every cmdlet in the session and in any module called by the function. Use-ErrorAction Stopon the specific cmdlet calls that should be catchable.
Comment-Based Help
Comment-based help is built-in documentation that works with Get-Help. It makes functions self-describing and is the difference between code that someone can use confidently and code they have to read to understand:
function Get-AutoStoppedService {
<#
.SYNOPSIS
Returns services set to start automatically that are not currently running.
.DESCRIPTION
Queries the specified computer for Windows services where StartType is
Automatic and Status is not Running. Useful for identifying services
that should be running but have stopped unexpectedly.
.PARAMETER ComputerName
The name of one or more computers to query. Defaults to the local computer.
.EXAMPLE
Get-AutoStoppedService
Returns stopped automatic-start services on the local computer.
.EXAMPLE
Get-AutoStoppedService -ComputerName "Server01", "Server02"
Returns stopped automatic-start services on the specified computers.
.NOTES
Requires WinRM access to remote computers.
#>
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
foreach ($Computer in $ComputerName) {
Write-Verbose "Querying services on: $Computer"
Get-Service -ComputerName $Computer |
Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' }
}
}
Once this is in a session, Get-Help Get-AutoStoppedService -Full displays the synopsis, full description, parameter descriptions, and examples — exactly as it does for built-in cmdlets.
The recognised comment-based help keywords are .SYNOPSIS, .DESCRIPTION, .PARAMETER <name>, .EXAMPLE, .INPUTS, .OUTPUTS, .NOTES, and .LINK. The comment block must appear immediately before or inside the function.
Finding and Removing Loaded Functions
Every function loaded into a PowerShell session is visible through the Function: PSDrive:
# List all loaded functions whose names contain "Service"
Get-ChildItem -Path Function:\*Service*
# Remove specific functions from the current session
Get-ChildItem -Path Function:\Get-*Version | Remove-Item
# Verify removal
Get-ChildItem -Path Function:\Get-*Version
Get-ChildItem -Path Function:\*Service* searches the Function: provider for any loaded function matching the wildcard pattern. Remove-Item removes the match from the current session — the source file on disk is not affected. This is useful for reloading a modified function without restarting the session.
Function Design Best Practices
Do one thing. A function named Export-UserReport should export a report, not also send an email and write to the event log. Functions that do many things are harder to test, reuse, and debug.
Accept input through parameters, not hardcoded values. A function with $server = "prod-01" baked in cannot be reused. A function with [Parameter(Mandatory)][string]$ComputerName works everywhere.
Use approved verbs. Unapproved verbs generate import warnings and hurt discoverability.
Always add [CmdletBinding()] to functions intended for reuse. The cost is zero; the benefit is common parameters, proper pipeline support infrastructure, and the ability to add SupportsShouldProcess.
Add SupportsShouldProcess to any function that modifies state. Users and automation scripts should always be able to run a function with -WhatIf before committing to a change.
Validate at the parameter level, not inside the body. [ValidateSet()], [ValidateRange()], and [Parameter(Mandatory)] produce consistent, helpful errors before the function body ever runs.
Use Write-Verbose for tracing. Inline comments are invisible at runtime. Write-Verbose is off by default and on when needed.
Add comment-based help. Writing .SYNOPSIS and .EXAMPLE costs two minutes and makes the function usable for anyone with Get-Help.
Use try/catch with -ErrorAction Stop on cmdlets that could fail. Unhandled exceptions terminate the caller with no explanation.
Common Mistakes to Avoid
Using unapproved verbs. The function works but generates module import warnings and cannot be discovered with Get-Command -Verb.
Relying on positional parameters in reusable functions. Positional arguments work interactively but make scripts unreadable. Always use named parameters in scripts that will be maintained.
Hardcoding computer names, paths, or credentials. A function with a hardcoded value is a script, not a reusable tool. All variable inputs should be parameters.
Not using [CmdletBinding()]. Without it, common parameters like -Verbose and -ErrorAction are unavailable. Add it to every function intended for reuse — even if param() is currently empty.
Modifying $ErrorActionPreference globally. Change error action per cmdlet with -ErrorAction Stop, not globally. Global changes affect every other command in the session.
Returning values with Write-Host. Write-Host writes directly to the console and bypasses the pipeline — its output cannot be captured in a variable or piped to another command. Use Write-Output, implicit output, or return for data that callers need to consume.
Found this helpful? Share the knowledge...
... or support this project to keep it alive!
Previous
Functions and Modular Code
Next
Parameter Binding
-
Basic Function Syntax
-
Function Naming Conventions
-
Parameters
-
Returning Values
-
Advanced Functions: Adding
[CmdletBinding()]SupportsShouldProcess— Adding-WhatIfand-Confirm
-
Parameter Validation
Mandatory— require a value- Array parameters — accept multiple values
- Default values
- Common validation attributes
-
Verbose Output
-
Pipeline Input
ValueFromPipeline— bind by object typeValueFromPipelineByPropertyName— bind by property namebegin,process, andend
-
Error Handling with
try/catch -
Comment-Based Help
-
Finding and Removing Loaded Functions
-
Function Design Best Practices
-
Common Mistakes to Avoid
