On this page

PowerShell Functions – From Basic to Advanced

Updated on June 11, 2026

Enrico Hochkoeppler Profile

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:


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 Stop on 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!

Buy Me a Coffee

Previous

Functions and Modular Code

Next

Parameter Binding