Progress pill
Rust & Bitcoin

Error model

  • Panic and Its Appropriate Uses
  • Working with Result and Option Types
  • Advanced Error Handling Patterns
  • External Libraries and Error Handling Ecosystems
Rust provides a comprehensive approach to error handling that balances safety with practicality. While the general error model concepts apply across programming languages, Rust offers specific tools and patterns that make error handling both explicit and manageable. Understanding these mechanisms is crucial for writing robust Rust applications that can gracefully handle unexpected situations while maintaining performance and safety.

Panic and Its Appropriate Uses

Rust's panic mechanism represents the most direct way to handle unrecoverable errors. When you call the panic! macro, the program immediately stops execution, either aborting or unwinding depending on your configuration. The panic macro accepts a string message that describes what went wrong, providing context for debugging. Additionally, methods like unwrap() and expect() on Result and Option types serve as shortcuts to panic when these types contain error values or None respectively. The expect() method allows you to provide a custom message, making it slightly more informative than unwrap() when debugging failures.
Despite its simplicity, panic should be used judiciously in production code. There are several scenarios where panic is not only acceptable but recommended. When writing examples or prototypes, panic provides a clean way to focus on the core functionality without cluttering the code with comprehensive error handling. In testing environments, panic is often the desired behavior when assertions fail, as it clearly indicates that something unexpected occurred. The Rust community also acknowledges situations where developers have more knowledge than the compiler, such as when parsing hard-coded IP addresses that are known to be valid.
However, the apparent safety of "compiler-verified" panics can be deceptive. Consider a scenario where you hard-code an IP address and use expect() because you know it's valid. Over time, as code evolves, that hard-coded value might be refactored into a constant, and later that constant might be changed to something like "localhost" for better user experience. Suddenly, your "safe" panic becomes a runtime failure. This evolution demonstrates why it's generally better to avoid panics in production code and instead return appropriate error types that can be handled gracefully.
One notable exception to the "avoid panic" rule involves mutex operations. When you call lock() on a mutex, it returns a Result because the lock can fail if another thread panicked while holding the mutex. This creates a confusing situation where your local code receives an error for something that happened in a completely different context. Since you cannot reasonably handle an error that originated from another thread's panic, many developers consider it acceptable to unwrap mutex locks, especially if you maintain a panic-free codebase elsewhere.

Working with Result and Option Types

The Result type forms the backbone of Rust's error handling system. As an enum that can hold either an Ok(value) or an Err(error), Result forces you to explicitly acknowledge that operations can fail. The Option type serves a similar purpose for cases where a value might simply be absent, containing either Some(value) or None. While Option doesn't provide detailed error information, it's perfect for situations where the absence of a value is meaningful and expected.
Both Result and Option provide several utility methods that make error handling more ergonomic. The unwrap_or() method returns the contained value if present, or a default value if there's an error or None. This pattern is particularly useful when you have a reasonable fallback, such as parsing user input with a sensible default when parsing fails. The unwrap_or_default() method works similarly but uses the type's default value instead of requiring you to specify one. While these methods don't technically handle errors in the traditional sense, they provide a way to gracefully degrade functionality when problems occur.
The question mark operator (?) is a concise syntax for error propagation. When applied to a Result or Option, it extracts the success value if present, or immediately returns the error from the current function if there's a problem. This operator eliminates the verbose error checking patterns common in languages like Go, where you must manually check and return errors at every step. The question mark operator essentially provides syntactic sugar for early returns, allowing you to write clean, linear code that focuses on the happy path while automatically handling error propagation.

Advanced Error Handling Patterns

The map() method on Result and Option types enables functional-style error handling that can make code more expressive and composable. When you call map() on a Result, the provided function is applied to the success value if present, while errors are automatically propagated without modification. This pattern is useful when chaining operations, as you can focus on transforming values without repeatedly handling error cases. The map_err() method provides the inverse functionality, allowing you to transform error types while leaving success values unchanged.
Error transformation becomes crucial when building layered applications where different components need different error types. Consider a function that parses user input and needs to convert low-level parsing errors into domain-specific errors. Using map_err(), you can easily translate a generic "invalid number format" error into a more contextual "invalid age" error that makes sense within your application's domain. This transformation happens right at the point where the error occurs, making the code more readable and maintainable than traditional try-catch blocks where error handling is separated from the operations that can fail.
The combination of the question mark operator with error mapping creates concise error handling patterns. You can chain operations, transform errors as needed, and propagate them up the call stack with minimal boilerplate. This approach keeps error handling close to the operations that can fail while maintaining clean separation between success and error paths.
use std::fmt; // Layered error types for a wallet application #[derive(Debug)] enum NetworkError { ConnectionFailed(String), Timeout, } #[derive(Debug)] enum WalletError { Network(NetworkError), InvalidAddress(String), InsufficientFunds { required: u64, available: u64 }, } // Implement Display for user-friendly messages impl fmt::Display for WalletError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { WalletError::Network(e) => write!(f, "Network error: {:?}", e), WalletError::InvalidAddress(addr) => write!(f, "Invalid address: {}", addr), WalletError::InsufficientFunds { required, available } => write!(f, "Need {} sats but only have {} available", required, available), } } } // Convert from lower-level error to domain error impl From<NetworkError> for WalletError { fn from(err: NetworkError) -> Self { WalletError::Network(err) } } // Simulated network call fn fetch_balance(address: &str) -> Result<u64, NetworkError> { if address.starts_with("bc1") { Ok(500_000) // 500k sats } else { Err(NetworkError::ConnectionFailed("Invalid endpoint".into())) } } // Higher-level function using ? with automatic error conversion fn send_payment(from: &str, amount: u64) -> Result<String, WalletError> { let balance = fetch_balance(from)?; // NetworkError auto-converts to WalletError if balance < amount { return Err(WalletError::InsufficientFunds { required: amount, available: balance, }); } Ok(format!("Sent {} sats", amount)) } fn main() { match send_payment("bc1qtest...", 100_000) { Ok(msg) => println!("Success: {}", msg), Err(e) => println!("Failed: {}", e), // User-friendly message } }

External Libraries and Error Handling Ecosystems

The Rust ecosystem includes several popular libraries that extend the standard library's error handling capabilities. The anyhow library provides a simplified approach to error handling by offering a universal error type that can automatically convert from any error type that implements the standard Error trait. This automatic conversion allows you to use the question mark operator with different error types without manual conversion, making it particularly useful for applications where you don't need to programmatically distinguish between different error types.
While anyhow excels at simplifying error handling for applications where errors are primarily displayed to users, it has limitations in library development. Since anyhow essentially converts all errors to string messages, consumers of your library cannot easily programmatically respond to different error conditions. This limitation makes anyhow more suitable for end-user applications than for libraries that need to provide structured error information to their consumers.
More advanced error handling approaches involve creating custom error types that model the specific failure modes of your application or library. A well-designed error model might distinguish between invalid input (which the caller can fix), runtime errors (which might be retryable), and permanent failures (which indicate bugs or unrecoverable conditions). This structured approach enables consumers of your code to make intelligent decisions about how to respond to different types of failures, whether that means retrying operations, prompting users for different input, or reporting bugs to developers.
Quiz
Quiz1/5
What is the primary difference between unwrap_or() and unwrap_or_default() methods when handling Result and Option types?