Error Handling Patterns
Idempotency, error handling, and dry-run patterns used in the Rust core engine and shell wrappers.
Principles
- Idempotent: Re-running produces the same result without side effects
- Defensive: Check existing state before making changes
- Fail-Fast: Errors propagate via
anyhow::Result; task failures are recorded, not fatal - Dry-Run: Preview mode shows what would change without modifications
Rust Error Handling
anyhow::Result
All fallible functions return anyhow::Result. Add context with .context():
packages::load(&conf.join("packages.toml"), active_categories)
.context("loading packages.toml")?;
ResourceError in Resources
Resource implementations (resources/*.rs) should return typed ResourceError variants
instead of anyhow::bail!(). This enables categorize_error() in the processing pipeline
to classify failures for diagnostic logging:
use crate::error::ResourceError;
// Platform-unsupported operations:
Err(ResourceError::NotSupported {
reason: "registry operations are only supported on Windows".to_string(),
}.into())
// External command failures:
Err(ResourceError::CommandFailed {
program: "pacman".to_string(),
message: format!("exit code {code}"),
}.into())
Available variants: CommandFailed, PermissionDenied, ConflictingState, NotSupported.
Task Failure Recording
Task failures don't abort the run. tasks::execute() catches errors and records TaskStatus::Failed; remaining tasks still execute. The summary reports all failures at the end.
Intentionally Ignored Errors
Use .ok() with a comment, not let _ =:
fs::remove_file(&path).ok(); // Cleanup: ignore if already removed
For operations that can legitimately fail but deserve logging, use if let Err:
if let Err(e) = fs::remove_file(&path) {
ctx.log.debug(&format!("Could not remove {}: {e}", path.display()));
}
Idempotency in Tasks
Resource-Based Tasks (preferred)
For tasks that manage declarative resources (Resource trait), use the generic
process_resources() / process_resource_states() helpers. They enforce the
correct check→dry-run→apply order automatically:
fn run(&self, ctx: &Context) -> Result<TaskResult> {
let items = ctx.config_read().items.clone();
let resources = items.iter()
.map(|entry| MyResource::from_entry(entry, &*ctx.executor));
process_resources(ctx, resources, &ProcessOpts::lenient("install"))
}
ProcessOpts controls behaviour per state variant via a ProcessMode enum:
Strict— fix missing + incorrect, bail on errors (symlinks, hooks, git config)Lenient— fix missing + incorrect, warn on errors (packages, registry)InstallMissing— only fix missing, warn on errorsFixExisting— only fix incorrect, bail on errors
See the rust-patterns skill for full ProcessMode / ProcessOpts reference.
Custom Tasks (non-resource)
For tasks that don't use the Resource trait, write the check→dry-run→mutate
loop manually:
fn run(&self, ctx: &Context) -> Result<TaskResult> {
if already_in_desired_state() {
return Ok(TaskResult::Ok);
}
if ctx.dry_run {
ctx.log.dry_run("would do something");
return Ok(TaskResult::DryRun);
}
perform_mutation()?;
Ok(TaskResult::Ok)
}
Pattern Order
- Check if already in desired state → skip or count as
already_ok - Check dry-run flag → log and return
DryRun - Perform the mutation →
Ok
This order ensures dry-run never mutates, and re-runs skip completed work.
Shell Wrapper Error Handling
The shell wrappers (dotfiles.sh, dotfiles.ps1) are thin but strict:
dotfiles.shusesset -o errexitandset -o nounsetdotfiles.ps1uses$ErrorActionPreference = 'Stop'
Both verify checksums after downloading binaries and fall back to existing binaries when GitHub is unreachable.
Rules
- Use
anyhow::Resultwith.context()for all fallible Rust code - Use
process_resources()/process_resource_states()for resource-based tasks — they enforce idempotency and dry-run automatically - Check existing state before mutations (idempotency) in custom tasks
- Check
ctx.dry_runbefore any side effect in custom tasks - Use
.ok()with a comment for intentionally ignored errors - Use
if let Err(e)with debug logging for errors worth noting - Return
TaskResultvariants correctly:Skipped,DryRun,Ok - Don't abort on task failure — record and continue
Related
rust-patternsskill — Task trait and Context structlogging-patternsskill — Logger API and task recording
Converted and distributed by TomeVault — claim your Tome and manage your conversions.