| title | Functions |
|---|---|
| description | Advanced functions — CmdletBinding, typed and validated parameters, pipeline blocks, ShouldProcess, and required comment-based help. |
Functions are the primary unit of PowerShell. Write advanced functions, not basic ones — the advanced-function machinery gives callers -Verbose, -ErrorAction, -WhatIf, and discoverable help for free.
Every function body follows the same order, so any reader — or agent — knows where to look:
- Comment-based help, first, inside the body.
[OutputType()]and[CmdletBinding()](withSupportsShouldProcesswhen it mutates state).param()block — mandatory parameters first.begin/process/endblocks for pipeline functions; a single body otherwise.
function Get-UserData {
<#
.SYNOPSIS
Get a user by id.
.DESCRIPTION
Return the user record for the given id.
.EXAMPLE
Get-UserData -UserId 'jdoe'
Returns the record for the user 'jdoe'.
.OUTPUTS
[PSCustomObject]
#>
[OutputType([PSCustomObject])]
[CmdletBinding()]
param(
# The unique identifier of the user.
[Parameter(Mandatory, Position = 0)]
[ValidateNotNullOrEmpty()]
[string] $UserId,
# Include deleted users in the result.
[Parameter()]
[switch] $IncludeDeleted
)
process {
# ...
}
}- Type every parameter and validate at the boundary —
[Parameter(Mandatory)],[ValidateSet(...)],[ValidateNotNullOrEmpty()]— so bad input is rejected early, not deep in the call stack. - Give every parameter a
[Parameter()]attribute, even when it carries no arguments — it is whereMandatory,ValueFromPipeline, and the rest attach, and it keeps every parameter declared the same way. - Attribute order, each on its own line:
[Parameter()], then validation attributes, then[ArgumentCompleter()], then[Alias()], then the typed declaration. - Separate parameters with a blank line, so each one's inline doc comment, attributes, and typed declaration read as a single block.
[switch]for boolean flags — never a[bool]parameter.- Name every parameter set with an intent-revealing name when a function has more than one mode; never
Defaultor__AllParameterSets. SetDefaultParameterSetNameto the most common intent.
- Guard mutations with
ShouldProcess. A function that creates, changes, or deletes state declares[CmdletBinding(SupportsShouldProcess)]and wraps the change inif ($PSCmdlet.ShouldProcess(...)), so-WhatIfand-Confirmwork. Never addSupportsShouldProcessto read-only verbs (Get,Test,Resolve). - Design for the pipeline. Functions that process collections accept
ValueFromPipelineinput and do the work in aprocessblock, streaming output rather than buffering it.
throwfor terminating errors;Write-Erroronly where the caller is expected to handle a non-terminating one.- Call cmdlets you mean to trap with
-ErrorAction Stopso they raise terminating, catchable errors. Native commands report failure through$LASTEXITCODE, not the error stream, so check it andthrowyourself — or set$PSNativeCommandUseErrorActionPreference = $trueon PowerShell 7.4+ so their non-zero exits honour$ErrorActionPreferencetoo. - Put the whole transaction in the
tryblock rather than setting success flags to gate later code, and do not lean on$?— it reports only whether the last command considered itself successful, with no detail. - In a
catch, copy$_into your own variable first, before later commands overwrite it. The baseline rules — fail fast, never swallow — live in Error Handling.
Send each kind of message to the stream built for it, so a caller can capture, redirect, or silence it:
- Results are objects on the output stream — emit them implicitly by naming the object on its own line; do not use
return $objto emit, and in a pipeline function emit fromprocess, notend. - Emit one object type, matching
[OutputType()]. Write-Verbosefor status a caller may want (-Verbose),Write-Debugfor maintainer breadcrumbs (-Debug), andWrite-Progressfor progress that need not persist.Write-WarningandWrite-Errorfor warnings and non-terminating errors.Write-Hostonly forShow-orFormat-verbs or an interactive prompt — never for data another command might consume.
[CmdletBinding()] is what turns on the -Verbose and -Debug switches, so those streams reach the caller.
Every function carries comment-based help — including internal and private helpers, not only the public surface. It is what lets a reader or an agent understand what the function does and how to call it without reading its body, and a private helper needs that as much as a public command does. Put it first inside the body, with sections in this order: .SYNOPSIS (one imperative sentence), .DESCRIPTION, at least one .EXAMPLE per behaviour, then .INPUTS, .OUTPUTS (matching [OutputType()]), .NOTES, .LINK. Document each parameter with an inline comment above it rather than a .PARAMETER block, and let comments explain why, not what.