16.4 Fallible Conversions: TryFrom and TryInto

When a conversion might fail (e.g., due to potential data loss, invalid input values, or unmet invariants), Rust employs the TryFrom<T> and TryInto<U> traits. These methods return a Result<TargetType, ErrorType>, explicitly forcing the caller to handle the possibility of conversion failure.

  • impl TryFrom<T> for U defines a conversion from T to U that might fail, returning Ok(U) on success or Err(ErrorType) on failure.
  • If TryFrom<T> is implemented for U, the compiler automatically provides TryInto<U> for T.

16.4.1 Standard Library Examples

Converting between numeric types where the target type has a narrower range is a prime use case:

use std::convert::{TryFrom, TryInto}; // Must import the traits

fn main() {
    let large_value: i32 = 1000;
    let small_value: i32 = 50;
    let negative_value: i32 = -10;

    // Try converting i32 to u8 (valid range 0-255)
    match u8::try_from(large_value) {
        Ok(v) => println!("{} converted to u8: {}", large_value, v),
        // This arm won't execute
        Err(e) => println!("Failed to convert {} to u8: {}", large_value, e),
        // Error: out of range
    }

    match u8::try_from(small_value) {
        Ok(v) => println!("{} converted to u8: {}", small_value, v), // Success: 50
        Err(e) => println!("Failed to convert {} to u8: {}", small_value, e),
    }

    // Using try_into() often requires type annotation if not inferable
    let result: Result<u8, _> = negative_value.try_into();
    // Inferred error type std::num::TryFromIntError
    match result {
        Ok(v) => println!("{} converted to u8: {}", negative_value, v),
        Err(e) => println!("Failed to convert {} to u8: {}", negative_value, e),
        // Error: out of range (negative)
    }
}

The specific error type (like std::num::TryFromIntError for standard numeric conversions) provides context about the failure.

16.4.2 Implementing TryFrom for Custom Types

Implement TryFrom to handle conversions that involve validation or potential failure for your types:

use std::convert::{TryFrom, TryInto};
use std::num::TryFromIntError; // Error type for standard int conversion failures

// A type representing a percentage (0-100)
#[derive(Debug, PartialEq)]
struct Percentage(u8);

#[derive(Debug, PartialEq)]
enum PercentageError {
    OutOfRange,
    ConversionFailed(TryFromIntError), // Wrap the underlying error if needed
}

// Allow conversion from i32, failing if outside 0-100 range
impl TryFrom<i32> for Percentage {
    type Error = PercentageError; // Associated error type for this conversion

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value < 0 || value > 100 {
            Err(PercentageError::OutOfRange)
        } else {
            // We know value is in 0..=100, so 'as u8' is safe here.
            // Alternatively, use u8::try_from for maximum safety, mapping the error.
            match u8::try_from(value) {
                Ok(val_u8) => Ok(Percentage(val_u8)),
                Err(e) => Err(PercentageError::ConversionFailed(e)),
                // Should not happen if range check is correct
            }
            // Simpler, given the check: Ok(Percentage(value as u8))
        }
    }
}

fn main() {
    assert_eq!(Percentage::try_from(50), Ok(Percentage(50)));
    assert_eq!(Percentage::try_from(100), Ok(Percentage(100)));
    assert_eq!(Percentage::try_from(101), Err(PercentageError::OutOfRange));
    assert_eq!(Percentage::try_from(-1), Err(PercentageError::OutOfRange));

    // Using try_into()
    let p_result: Result<Percentage, _> = 75i32.try_into();
    assert_eq!(p_result, Ok(Percentage(75)));

    let p_fail: Result<Percentage, _> = (-5i32).try_into();
    assert_eq!(p_fail, Err(PercentageError::OutOfRange));
}

Using TryFrom/TryInto leads to more robust code by making potential conversion failures explicit and requiring error handling.