Skip to content

Commit bc95ebd

Browse files
Copilotjohlju
andcommitted
Update error handling documentation in CONTRIBUTING.md
Co-authored-by: johlju <[email protected]>
1 parent 04f4f59 commit bc95ebd

File tree

2 files changed

+150
-59
lines changed

2 files changed

+150
-59
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4444
- `Set-SqlDscDatabaseProperty`
4545
- Updated comment-based help to reference correct enum values.
4646
- Added SQL Server version requirements to version-specific parameter help.
47+
- Updated CONTRIBUTING.md error handling documentation to clarify proper usage
48+
of `Write-Error` vs `$PSCmdlet.ThrowTerminatingError()` in public commands
49+
([issue #2193](https://github.com/dsccommunity/SqlServerDsc/issues/2193)).
4750

4851
### Fixed
4952

CONTRIBUTING.md

Lines changed: 147 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -308,85 +308,173 @@ If neither of those commands works in the scenarion then `throw` shall be used.
308308
Commands are publicly exported commands from the module, and the source for
309309
commands are located in the folder `./source/Public`.
310310

311-
#### Non-Terminating Error
311+
#### Error Handling Guidelines
312312

313-
A non-terminating error should only be used when a command shall be able to
314-
handle (ignoring) an error and continue processing and still give the user
315-
an expected outcome.
313+
Public commands should primarily use `Write-Error` for error handling, as it
314+
provides the most flexible and predictable behavior for callers. The statement
315+
`throw` shall never be used in public commands.
316316

317-
With a non-terminating error the user is able to decide whether the command
318-
should throw or continue processing on error. The user can pass the
319-
parameter and value `-ErrorAction 'SilentlyContinue'` to the command to
320-
ignore the error and allowing the command to continue, for example the
321-
command could then return `$null`. But if the user passes the parameter
322-
and value `-ErrorAction 'Stop'` the same error will throw a terminating
323-
error telling the user the expected outcome could not be achieved.
317+
##### When to Use Write-Error
324318

325-
The below example checks to see if a database exist, if it doesn't a
326-
non-terminating error are called. The user is able to either ignore the
327-
error or have it throw depending on what value the user specifies
328-
in parameter `ErrorAction` (or `$ErrorActionPreference`).
319+
Use `Write-Error` in most scenarios where a public command encounters an error:
320+
321+
- **Validation failures**: When input parameters fail validation
322+
- **Resource not found**: When a requested resource doesn't exist
323+
- **Operation failures**: When an operation cannot be completed
324+
- **Permission issues**: When access is denied
325+
326+
`Write-Error` generates a non-terminating error by default, allowing the caller
327+
to control behavior via the `-ErrorAction` parameter. This is the expected
328+
PowerShell pattern.
329+
330+
**Example**: A command that retrieves a database returns an error if not found:
329331

330332
```powershell
331333
if (-not $databaseExist)
332334
{
333335
$errorMessage = $script:localizedData.MissingDatabase -f $DatabaseName
334336
335-
Write-Error -Message $errorMessage -Category 'InvalidOperation' -ErrorId 'GS0001' -TargetObject $DatabaseName
337+
Write-Error -Message $errorMessage -Category 'ObjectNotFound' -ErrorId 'GSD0001' -TargetObject $DatabaseName
338+
339+
return
336340
}
337341
```
338342

339-
#### Terminating Error
343+
The caller controls the behavior:
340344

341-
A terminating error is an error that the user are not able to ignore by
342-
passing a parameter to the command (like for non-terminating errors).
345+
```powershell
346+
# Returns $null, continues execution (non-terminating)
347+
$db = Get-Database -Name 'NonExistent'
343348
344-
If a command shall throw an terminating error then the statement `throw` shall
345-
not be used, neither shall the command `Write-Error` with the parameter
346-
`-ErrorAction Stop`. Always use the method `$PSCmdlet.ThrowTerminatingError()`
347-
to throw a terminating error. The exception is when a `[ValidateScript()]`
348-
has to throw an error, then `throw` must be used.
349+
# Throws terminating error, stops execution
350+
$db = Get-Database -Name 'NonExistent' -ErrorAction 'Stop'
351+
```
349352

350353
> [!IMPORTANT]
351-
> Below output assumes `$ErrorView` is set to `'NormalView'` in the
352-
> PowerShell session.
353-
354-
When using `throw` it will fail on the line with the throw statement
355-
making it look like it is that statement inside the function that failed,
356-
which is not correct since it is either a previous command or evaluation
357-
that failed resulting in the line with the `throw` being called. This is
358-
an example when using `throw`:
359-
360-
```plaintext
361-
Exception:
362-
Line |
363-
2 | throw 'My error'
364-
| ~~~~~~~~~~~~~~~~
365-
| My error
366-
```
354+
> Always use `return` after `Write-Error` to prevent further processing in the
355+
> command. Without `return`, the command continues executing, which may cause
356+
> unexpected behavior or additional errors.
357+
358+
##### Understanding $PSCmdlet.ThrowTerminatingError
367359

368-
When instead using `$PSCmdlet.ThrowTerminatingError()`:
360+
The `$PSCmdlet.ThrowTerminatingError()` method is **not recommended** for public
361+
commands despite its name suggesting it throws a terminating error. It actually
362+
throws a **command-terminating error** which has unexpected behavior when called
363+
from another command.
364+
365+
**The Problem**: When a command uses `$PSCmdlet.ThrowTerminatingError()`, the
366+
error behaves like a non-terminating error to the caller unless the caller
367+
explicitly uses `-ErrorAction 'Stop'`. This creates confusing behavior:
369368

370369
```powershell
371-
$PSCmdlet.ThrowTerminatingError(
372-
[System.Management.Automation.ErrorRecord]::new(
373-
'MyError',
374-
'GS0001',
375-
[System.Management.Automation.ErrorCategory]::InvalidOperation,
376-
'MyObjectOrValue'
370+
function Get-Something
371+
{
372+
[CmdletBinding()]
373+
param ()
374+
375+
$PSCmdlet.ThrowTerminatingError(
376+
[System.Management.Automation.ErrorRecord]::new(
377+
'Database not found',
378+
'GS0001',
379+
[System.Management.Automation.ErrorCategory]::ObjectNotFound,
380+
'MyDatabase'
381+
)
377382
)
378-
)
383+
384+
# This line never executes (correctly stopped)
385+
Write-Output 'After error'
386+
}
387+
388+
function Start-Operation
389+
{
390+
[CmdletBinding()]
391+
param ()
392+
393+
Get-Something
394+
395+
# BUG: This line DOES execute even though Get-Something threw an error!
396+
Write-Output 'Operation completed'
397+
}
398+
399+
# Caller must use -ErrorAction 'Stop' to prevent continued execution
400+
Start-Operation -ErrorAction 'Stop'
379401
```
380402

381-
The result from `$PSCmdlet.ThrowTerminatingError()` shows that the command
382-
failed (in this example `Get-Something`) and returns a clear category and
383-
error code.
384-
385-
```plaintext
386-
Get-Something : My Error
387-
At line:1 char:1
388-
+ Get-Something
389-
+ ~~~~~~~~~~~~~
390-
+ CategoryInfo : InvalidOperation: (MyObjectOrValue:String) [Get-Something], Exception
391-
+ FullyQualifiedErrorId : GS0001,Get-Something
403+
Because of this behavior, **use `Write-Error` instead** in public commands.
404+
405+
##### When $PSCmdlet.ThrowTerminatingError Can Be Used
406+
407+
Use `$PSCmdlet.ThrowTerminatingError()` only in these limited scenarios:
408+
409+
1. **Private functions** that are only called internally and where the behavior
410+
is well understood
411+
2. **Assert-style commands** (like `Assert-SqlDscLogin`) whose purpose is to
412+
throw on failure
413+
3. **Commands with `SupportsShouldProcess`** where an error in the
414+
`ShouldProcess` block must terminate (before state changes)
415+
416+
In these cases, ensure callers are aware of the behavior and use
417+
`-ErrorAction 'Stop'` when calling the command from other commands.
418+
419+
##### Exception Handling in Commands
420+
421+
When catching exceptions in try-catch blocks, re-throw errors using `Write-Error`
422+
with the original exception:
423+
424+
```powershell
425+
try
426+
{
427+
$database.Create()
428+
}
429+
catch
430+
{
431+
$errorMessage = $script:localizedData.CreateDatabaseFailed -f $DatabaseName
432+
433+
Write-Error -Message $errorMessage -Category 'InvalidOperation' -ErrorId 'CD0001' -TargetObject $DatabaseName -Exception $_.Exception
434+
435+
return
436+
}
392437
```
438+
439+
##### Pipeline Processing Considerations
440+
441+
For commands that accept pipeline input and process multiple items, use
442+
`Write-Error` to allow processing to continue for remaining items:
443+
444+
```powershell
445+
process
446+
{
447+
foreach ($item in $Items)
448+
{
449+
if (-not (Test-ItemValid $item))
450+
{
451+
Write-Error -Message "Invalid item: $item" -Category 'InvalidData' -ErrorId 'PI0001' -TargetObject $item
452+
continue
453+
}
454+
455+
# Process valid items
456+
Process-Item $item
457+
}
458+
}
459+
```
460+
461+
The caller can control whether to stop on first error or continue:
462+
463+
```powershell
464+
# Continue processing all items, collect errors
465+
$results = $items | Process-Items
466+
467+
# Stop on first error
468+
$results = $items | Process-Items -ErrorAction 'Stop'
469+
```
470+
471+
##### Summary
472+
473+
| Scenario | Use | Why |
474+
|----------|-----|-----|
475+
| General error handling in public commands | `Write-Error` | Provides consistent, predictable behavior; allows caller to control termination |
476+
| Pipeline processing with multiple items | `Write-Error` | Allows processing to continue for remaining items |
477+
| Assert-style commands | `$PSCmdlet.ThrowTerminatingError()` | Command purpose is to throw on failure |
478+
| Private functions (internal use only) | `$PSCmdlet.ThrowTerminatingError()` or `Write-Error` | Behavior is understood by internal callers |
479+
| Any command | Never use `throw` | Poor error messages; unpredictable behavior |
480+
| Parameter validation attributes | `throw` | Only valid option within `[ValidateScript()]` |

0 commit comments

Comments
 (0)