skip to content

Your team maintains a large shared PowerShell codebase. Would you mandate `Set-StrictMode` together with typed, validated parameters across it, and what does that discipline cost?

level: principalimportance: nice to knowfreq 24%

answer

  1. the interactive default is the wrong production default
  2. a typo becomes $null, not an error
  3. the setting is per-scope, not global
  4. annotating a type is not the same as validating a value
  5. Latest is a moving target

basics

~20 s

Usually yes for shared code: PowerShell is permissive by default, so typos in variable and property names return $null silently and fail far from the cause. Strict mode and validation attributes convert those into immediate errors, at the cost of rejecting some dynamic idioms and of version-sensitive behaviour.

solid answer

~50 s

By default PowerShell is deliberately forgiving — an unassigned variable is `$null`, a misspelled property is `$null`, and both flow onward until something far downstream misbehaves. In shared automation that is the wrong default, so I would set `Set-StrictMode -Version 3.0` at the top of each script and module and gate parameters with `[ValidateSet()]`, `[ValidateRange()]` and `[ValidateNotNullOrEmpty()]`. The costs are real. Strict mode is scoped, so each file must opt in and coverage is easy to lose; `-Version Latest` is a moving target that can change behaviour when the engine is upgraded, so I pin a number. Type annotations *coerce* rather than validate — `[int]$Percent` happily accepts `'50'` and rounds `2.5` to `2` — so they are not the safety net people assume. And genuinely dynamic code that probes for optional properties needs rewriting. I would enforce it in new code and on touch, checked by PSScriptAnalyzer in CI, rather than as a big-bang migration.

code

powershell · 15 lines
powershell
Set-StrictMode -Version 3.0

function Test-Threshold {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, 100)]
        [int]$Percent,

        [ValidateSet('warn', 'fail')]
        [string]$Mode = 'warn'
    )
    "$Mode at $Percent percent"
}

Test-Threshold -Percent 50 -Mode 'fail'

go deeper

for a junior

Know that Set-StrictMode makes typos in variable and property names into errors instead of silent $null, and that validation attributes go above a parameter in the param block.

for a middle

Explain the version levels and the difference between coercion and validation — that [int] converts while [ValidateRange()] rejects — and name the attributes you would reach for.

for a senior

Show how you would deploy it: templates plus a CI lint check for coverage, a pinned version rather than Latest, and tests around paths where latent bugs will now surface loudly.

for a principal

Own the trade-off explicitly — permissiveness is right for interactive code and wrong for unattended code that changes production state — and stage the rollout so the migration cost lands when the team chose it.

## The default that makes PowerShell pleasant also makes it dangerous PowerShell was designed for people typing at a prompt, and interactive ergonomics favour permissiveness. Reference a variable you never assigned and you get `$null`, not an error. Ask for `$user.EmailAddres` when the property is `EmailAddress` and you get `$null`, not an error. Compare that `$null` to something and the comparison quietly succeeds or quietly fails. At a prompt this is fine — you see the empty result and retype. In an unattended script that provisions accounts or deletes resources, the same behaviour means a typo produces a wrong action rather than a stopped run, and the eventual failure appears far from the line that caused it. Choosing a stricter dialect for shared code is therefore a real engineering decision, not a style preference. ## What `Set-StrictMode` actually buys ```powershell Set-StrictMode -Version 3.0 ``` The versions are cumulative: - **1.0** — referencing an uninitialised variable is an error, except inside strings. - **2.0** — adds uninitialised variables inside strings; references to non-existent properties of an object; calling a function using method-call syntax (`Get-Thing($a, $b)`, a classic newcomer bug); and a variable reference without a name. - **3.0** — adds out-of-bounds and unresolvable array indexes. - **Latest** — whatever the newest rules are on the engine that happens to run the code. The non-existent-property rule from 2.0 is the one that catches most real defects, and the method-call-syntax rule catches a genuine class of silent misbehaviour where the arguments arrive as a single array. Two properties of the cmdlet shape how you deploy it. It applies to the scope in which it is called and that scope's children, so it is not a global switch — every script and module must set it for itself, and a file someone adds later without it is silently unprotected. And `Latest` means the rule set can change under you: upgrading the engine can turn previously running code into failing code. For shared automation, pin an explicit version and change it deliberately. ## What typed and validated parameters buy — and what they do not ```powershell function Test-Threshold { param( [Parameter(Mandatory = $true)] [ValidateRange(1, 100)] [int]$Percent, [ValidateSet('warn', 'fail')] [string]$Mode = 'warn' ) "$Mode at $Percent percent" } ``` The important distinction is that a **type annotation coerces**, while a **validation attribute rejects**. `[int]$Percent` does not mean "only integers are accepted" — PowerShell will convert `'50'` to `50`, and it will convert `2.5` to `2` using .NET's round-to-even, which surprises people who expected 3. `[string]` will happily stringify almost anything, so a caller passing an object gets its type name rather than an error. If you want a gate, you need `[ValidateRange()]`, `[ValidateSet()]`, `[ValidatePattern()]`, `[ValidateNotNullOrEmpty()]` or a `[ValidateScript()]`. `[ValidateSet()]` earns its place twice: it rejects bad values *and* it drives tab completion for callers, which is a usability win, not just a safety one. `Mandatory = $true` prevents the silent-empty-parameter class of bug that strict mode does not cover, because a parameter with no value is initialised, just empty. ## The costs, stated honestly - **Idiomatic dynamic code breaks.** Probing heterogeneous objects with `if ($obj.MaybeThere)` is a normal PowerShell pattern and becomes an error under 2.0+. It has to be rewritten with `$obj.PSObject.Properties.Name -contains 'MaybeThere'`, which is more correct and less readable. - **Coverage is per-file and easy to lose.** Because the setting is scoped, there is no single place to enforce it; you need a lint rule or a template. - **Version sensitivity.** `Latest` can shift behaviour on upgrade; a pinned version can lag behind useful new checks. Either way it is a decision someone must own. - **Rigidity at the boundary.** Tight validation on a widely called function makes every legitimate new use case a change to the function. That is usually the right trade, but it is a trade. - **Migration cost.** Turning strict mode on across an old codebase surfaces latent bugs all at once, at a time you did not choose. ## How I would actually roll it out Mandate it for new scripts and modules and for any file being substantially changed, rather than as a flag day. Put `Set-StrictMode -Version 3.0` in the file template so nobody has to remember. Run `Invoke-ScriptAnalyzer` in CI so the convention is checked rather than hoped for, and treat that as the enforcement mechanism instead of review comments. Add tests around the paths the migration touches, because strict mode turns silent wrong behaviour into loud failure — which is the point, but only helps if someone is watching when it fires. The underlying judgment is about audience. Code that only its author runs interactively benefits from permissiveness. Code that other people invoke, that runs unattended, or that changes production state should fail fast and loudly, and paying a little expressiveness for that is a good deal.

  • Why is `[int]$Percent` not the same as validating that the input is a valid percentage?
    Because a type annotation coerces rather than rejects. PowerShell converts `'50'` to `50`, and converts `2.5` to `2` with round-to-even, so the parameter accepts values you did not intend and silently changes others. Range checking needs `[ValidateRange(1, 100)]`; set membership needs `[ValidateSet()]`. Types constrain shape, attributes constrain value.
  • Why pin `Set-StrictMode -Version 3.0` rather than use `Latest`?
    Because `Latest` resolves to whatever rules the running engine has. Upgrading PowerShell can then add checks that turn working code into failing code, at a moment nobody chose, on a host you may not control. Pinning makes the dialect a deliberate, reviewable decision; you raise it when you have time to fix what it surfaces.
  • How do you get coverage when strict mode is per-scope?
    You cannot set it once centrally, so make it structural: put the line in the script and module templates, and enforce it with a lint check in CI — `Invoke-ScriptAnalyzer` plus a rule that the file opts in. Relying on reviewers to notice a missing line gives you coverage that decays quietly as the codebase grows.

saying these in an interview costs you the question

  • Claims strict mode is global once set anywhere in the session
  • Says [string] or [int] on a parameter rejects wrong input
  • Treats Latest as the obviously correct version to pin
  • Proposes a big-bang migration with no test coverage
  • Dismisses dynamic property probing as always wrong rather than a trade

context