Course contentsShow
← Rust
Lesson 404 of 1,9104. Lifetimes and Advanced OwnershipFree lesson

Refining Error Handling with Lifetimes

Propagate errors while maintaining valid references to the original context.

In this lesson, you will learn how to design error types that hold references to the data that caused the failure, ensuring your error reporting remains both memory-safe and efficient.

The Challenge of Context

When an operation fails, you often want to return more than just a string message; you want to return the specific part of the input that caused the issue. If you try to store a reference to the input inside your error type, the Rust compiler will stop you because it needs to know for how long that reference remains valid.

By using lifetimes, we can tell the compiler that our error type is "tied" to the input data it is describing.

Defining Lifetimes in Errors

Imagine a parser that fails when it finds an invalid character. Instead of copying the bad segment into a String (which is slow), we can store a reference to the original slice:

struct ParseError<'a> {
    invalid_part: &'a str,
    reason: &'static str,
}

fn validate_input<'a>(input: &'a str) -> Result<(), ParseError<'a>> {
    if input.contains('!') {
        Err(ParseError {
            invalid_part: &input[0..1], // Storing a slice of the input
            reason: "Exclamation marks are forbidden",
        })
    } else {
        Ok(())
    }
}

By adding the lifetime parameter 'a to ParseError, we satisfy the borrow checker. We are explicitly stating: "This error cannot outlive the string we are validating." This approach is powerful because it avoids heap allocations entirely. Your error is essentially just a pointer into your existing data.

Propagation with Constraints

When you propagate these errors using the ? operator, you must ensure that your function signatures correctly propagate those lifetime constraints. If you return an error, the function's return type must declare that the error contains a reference from the input. This pattern keeps your API transparent—anyone calling validate_input knows immediately that the returned error is borrowing from their original input string.

Key Takeaway: By binding your error types to the lifetime of the input data, you can create zero-copy error reporting that is both high-performance and strictly type-safe.