Nazara Error Handling Guide
Philosophy
Nazara follows a "fail gracefully" philosophy. We prefer returning descriptive errors over panicking, reserving panics only for truly catastrophic and unfixable situations.
Core Principles:
- Single Source of Truth: Error messages are defined once in
NazaraErrorvariants and displayed consistently - Immediate Feedback: Errors are logged immediately using the
failure!macro for user visibility - Clean Propagation: Most functions use the
?operator without logging noise - Context When Needed: Optional context prefixes indicate which module or operation triggered the error
The NazaraError Enum
All errors in Nazara are represented by the NazaraError enum in src/error.rs:
#![allow(unused)] fn main() { pub enum NazaraError { /// Something went wrong trying to parse DMI tables. Dmi(dmidecode::InvalidEntryPointError), /// Used to indicate that the collection of system data failed. UnableToCollectData(String), /// Used for handling errors during file operations. FileOpError(std::io::Error), /// Indicates that a required config option is missing from the config file. MissingConfigOptionError(String), /// An error occurred while accessing data returned by NetBox. NetBoxApiError(String), /// Expects a `String` message. Used for edge cases and general purpose error cases. Other(String), // ... other variants } }
Display Trait Implementation
Every NazaraError variant implements std::fmt::Display to provide user-friendly error messages:
#![allow(unused)] fn main() { impl std::fmt::Display for NazaraError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { NazaraError::FileOpError(err) => { write!(f, "File operation failed: {err}") } NazaraError::NetBoxApiError(msg) => { write!(f, "NetBox API Error: {msg}") } NazaraError::Other(msg) => f.write_str(&msg), // ... other variants } } } }
The Display implementation should never call failure! or any other logging macro. It should only format the error message.
Convenience Methods
The NazaraError enum provides two convenience methods for common error handling patterns:
fail() - Log and Return
#![allow(unused)] fn main() { impl NazaraError { /// Log this error with failure! and return it wrapped in Err(...) pub fn fail<T>(self) -> NazaraResult<T> { failure!("{}", self); Err(self) } } }
Purpose: Combines logging and returning into a single operation.
Example: Using the fail-function to log and return an error
Example: Using the fail-function to log and return an error
#![allow(unused)] fn main() { return NazaraError::Other("Config file contains invalid entries".to_owned()).fail(); }
log() - Log with Context
#![allow(unused)] fn main() { impl NazaraError { /// Log this error with additional context prefix. /// Used when we need to log multiple errors but continue processing to fail in the end /// either with the calling function or some other higher instance. /// /// The optional context in this case refers to the module or program part that the error /// occurred in. For example: [DHCP-Mode]. pub fn log(&self, context: Option<&str>) { if let Some(ctx) = context { failure!("[{}] {}", ctx, self); return; } failure!("{}", self); } } }
Purpose: Log errors with optional context when you need to accumulate multiple errors before failing.
Example: Logging an error but continue processing
Example: Logging an error but continue processing
#![allow(unused)] fn main() { // Accumulate errors, fail at the end for validation in validations { if !validation.is_valid() { let err = NazaraError::Other(format!("Validation '{}' failed", validation.name())); err.log(None); error_count += 1; } } if error_count > 0 { return NazaraError::Other("Multiple validations failed".to_owned()).fail(); } }
Example: Logging an error with context
Example: Logging an error with context
context indicates where or why this error has occurred. For example when working with the ip-mode flags,
errors rooted in the changes of IP addresses by DHCP or something similar have the context "DHCP-Mode".
#![allow(unused)] fn main() { let err = NazaraError::NetBoxApiError("IPv4 not found".to_owned()); err.log(Some("DHCP-Mode")); return Err(err); }
Examples
Example: Logging with Return
Example: Logging with Return
Use when an error is terminal and should be returned immediately.
#![allow(unused)] fn main() { return NazaraError::Other("Config file contains invalid entries".to_owned()).fail(); }
Example: Log with Context, Then Return
Example: Log with Context, Then Return
Use when the error needs additional module/operation context.
#![allow(unused)] fn main() { let err = NazaraError::NetBoxApiError( format!("IPv4 address \"{}\" was not registered in NetBox", ip) ); err.log(Some("DHCP-Mode")); return Err(err); }
Or in a closure (for use with ok_or_else):
#![allow(unused)] fn main() { let ipv4_id = search_ip(client, &ip.to_string(), None)?.ok_or_else(|| { let err = NazaraError::NetBoxApiError(format!("IPv4 \"{}\" not found", ip)); err.log(Some("DHCP-Mode")); err })?; }
Example: Accumulate Errors, Fail Later
Example: Accumulate Errors, Fail Later
Use when processing multiple items and collecting all errors before failing.
#![allow(unused)] fn main() { let mut config_errors = Vec::new(); for field in required_fields { if config.get(field).is_none() { let err = NazaraError::MissingConfigOptionError(field.to_string()); err.log(None); config_errors.push(err); } } if !config_errors.is_empty() { return NazaraError::Other("Missing required config fields".to_owned()).fail(); } }
Example: Custom User-Facing Messages
Example: Custom User-Facing Messages
When you need to provide specific user guidance, use failure! directly.
#![allow(unused)] fn main() { failure!( "Tag '{}' does not exist. Use --prepare-environment to create it.", tag_name ); }
This is clearer than trying to fit the guidance into a NazaraError variant's display message.
When to Use What
Use .fail() when:
- The error is terminal (nothing else can be done)
- The error message is sufficient (no additional context needed)
- You want clean, single-line error handling
- This is the 90% common case
Use .log() when:
- You need to accumulate multiple errors before failing
- You want to add module/operation context
- The error should be logged but processing should continue
- You need to track multiple issues
Use failure! directly when:
- You need a custom message with user guidance
- The message is temporary/debugging (will be replaced later)
- You're providing feedback before returning a different error
Don't use either when:
- You can simply return the error with
?(no logging needed) - The caller will handle the error (low-level utilities)
- The error will be logged by the caller anyway
Best Practices
1. Choose the Right Error Variant
Prefer specific variants over Other when possible:
#![allow(unused)] fn main() { // GOOD return NazaraError::MissingConfigOptionError("netbox_uri".to_owned()).fail(); // OK (when no specific variant fits) return NazaraError::Other("Custom error message".to_owned()).fail(); }
2. Include Context in Error Messages
When using Other, make the message descriptive:
#![allow(unused)] fn main() { // GOOD return NazaraError::Other("Config file contains invalid entries".to_owned()).fail(); // BAD return NazaraError::Other("Error".to_owned()).fail(); }
3. Use Context for Module Identification
When logging with context, use module or operation names:
#![allow(unused)] fn main() { err.log(Some("DHCP-Mode")); err.log(Some("Config-Parser")); }
4. Don't Duplicate Error Messages
The error message should be defined once—in the Display implementation or the error construction:
#![allow(unused)] fn main() { // GOOD let msg = "Config file contains invalid entries".to_owned(); failure!("{}", msg); return Err(NazaraError::Other(msg)); // REDUNDANT (duplicates the message) failure!("Config file contains invalid entries"); return NazaraError::Other("Config file contains invalid entries".to_owned()).fail(); }
5. Let Errors Propagate
For low-level utility functions, just return errors. Let the caller decide whether to log:
#![allow(unused)] fn main() { // In a low-level utility pub fn read_config_file() -> NazaraResult<ConfigData> { let mut file = File::open(get_config_path(true))?; // Just propagate // ... } // In the calling function let config = read_config_file() .map_err(|e| e.log(None))?; // Log here }
Error Flow
[Deep Function] → Creates/Returns NazaraError
↓ (via ? operator)
[Middle Function] → Propagates with ?
↓ (via ? operator)
[Application Logic] → Logs with .fail() or .log()
↓
[main.rs] → Catches error, logs with failure!
↓
[stderr] → User sees formatted error message
Example: Complete Error Handling Flow
Here's how error handling works in a real scenario:
// 1. Low-level function (no logging, just propagation) pub fn read_config_file() -> NazaraResult<ConfigData> { let mut file = File::open(get_config_path(true))?; let mut contents = String::new(); file.read_to_string(&mut contents)?; toml::from_str(&contents).map_err(NazaraError::DeserializationError) } // 2. Mid-level function (propagates errors) pub fn validate_config(config: &ConfigData) -> NazaraResult<()> { if config.netbox_uri.is_empty() { return Err(NazaraError::MissingConfigOptionError("netbox_uri".to_owned())); } Ok(()) } // 3. Application logic (logs and returns) pub fn setup_configuration() -> NazaraResult<ConfigData> { let config = read_config_file()?; validate_config(&config)?; if config.is_invalid() { return NazaraError::Other("Config validation failed".to_owned()).fail(); } Ok(config) } // 4. Main entry point (logs final error) fn main() { match nazara::run() { Ok(_) => success!("All done!"), Err(e) => failure!("{}", e), // Uses failure! for consistency } }
Remember: Errors should be logged once, at the appropriate level, with sufficient context.