In this lesson, you will learn how to design error-handling patterns that pass resource ownership into an error type to ensure that memory or system handles are never leaked when a process fails.
The Problem: Orphaned Resources
In many languages, when a function fails, it might return an error while "abandoning" the data it was currently processing. In Rust, we use the ownership model to our advantage: if a function fails, we can move the partially processed resource into the error itself. This ensures that the caller retains ownership of the resource even if the operation failed, allowing them to either retry the operation or clean up the resource properly.
Designing an Ownership-Aware Error
Consider a function that processes a file. If the processing fails halfway through, we don't want the file handle to simply disappear or leak. We want to return the handle back to the caller as part of the error.
struct FileHandle(String);
enum ProcessError {
InvalidData(FileHandle), // The error "owns" the handle
IoError,
}
fn process_file(file: FileHandle) -> Result<(), ProcessError> {
// Imagine some logic that fails here
let success = false;
if !success {
// We move the 'file' into the error variant
return Err(ProcessError::InvalidData(file));
}
Ok(())
}
Why This Matters
By embedding the resource inside the ProcessError, you create a "recoverable failure" state. Because the resource is now inside the error type, Rust’s ownership rules guarantee that it won't be dropped until the error itself is dropped. If the caller receives this error, they can access the FileHandle field within the error, perform a cleanup, or even re-attempt the task with the same object.
This pattern is a powerful way to write robust systems where "failure" isn't the end of a resource's lifecycle—it's just a transition back to the caller.
Key Takeaway: By moving resources into your error variants, you prevent leaks and empower the caller to recover or safely dispose of resources when an operation fails.