Telex logo Telex

Rust Patterns That Matter #2: From / Into Conversions

Post 2 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.

Previous: #1: Newtype | Next: #3: Error Handling

You're writing an API that accepts different input types. Some callers have a &str, some have a String, some have a PathBuf. You want all of them to work without forcing callers to convert manually. The From / Into traits are how idiomatic Rust APIs achieve this.

The motivation

struct Email {
    address: String,
}

impl Email {
    fn new(address: String) -> Self {
        Email { address }
    }
}

// Caller with a String: fine
let e = Email::new("a@b.com".to_string());

// Caller with a &str: has to convert manually
let e = Email::new("a@b.com".to_string()); // verbose

Every caller has to write .to_string(). It's small, but it's friction. In an API with many such functions, the friction adds up.

The pattern: From<T>

Implement From for each type you want to accept:

impl From<String> for Email {
    fn from(address: String) -> Self {
        Email { address }
    }
}

impl From<&str> for Email {
    fn from(address: &str) -> Self {
        Email { address: address.to_string() }
    }
}

Now callers can use .into():

let e: Email = "a@b.com".into();
let e: Email = String::from("a@b.com").into();

The blanket impl

When you implement From<T> for U, the standard library automatically provides Into<U> for T via a blanket impl. You never need to implement Into directly. Always implement From.

Using impl Into<T> in function signatures

The real payoff is in function parameters:

fn send_email(to: impl Into<Email>, body: impl Into<String>) {
    let to = to.into();
    let body = body.into();
    // ...
}

// All of these work:
send_email("a@b.com", "Hello");
send_email(Email::from("a@b.com"), String::from("Hello"));
send_email("a@b.com", "Hello".to_string());

The caller passes whatever type they have. The conversion happens inside the function. No ceremony at the call site.

Error handling: From powers ?

The ? operator uses From to convert errors. When you write:

fn read_config() -> Result<Config, MyError> {
    let text = std::fs::read_to_string("config.toml")?;
    let config: Config = toml::from_str(&text)?;
    Ok(config)
}

The first ? might produce an io::Error. The second might produce a toml::de::Error. The function returns MyError. The ? operator calls From::from() to convert each error type into MyError. If you've implemented From<io::Error> for MyError and From<toml::de::Error> for MyError, it just works.

This is the foundation of the error handling pattern in #3. thiserror generates these From impls for you.

TryFrom / TryInto

For conversions that can fail, use TryFrom:

struct Port(u16);

impl TryFrom<u32> for Port {
    type Error = String;

    fn try_from(value: u32) -> Result<Self, Self::Error> {
        if value > 65535 {
            Err(format!("{value} is not a valid port"))
        } else {
            Ok(Port(value as u16))
        }
    }
}

let p: Result<Port, _> = 8080_u32.try_into(); // Ok(Port(8080))
let p: Result<Port, _> = 99999_u32.try_into(); // Err("99999 is not a valid port")

When to use it

When not to: if the conversion is lossy, surprising, or has side effects, make it an explicit named method. From / Into imply a natural, lossless conversion. If .into() would silently truncate data or change semantics, use a named method instead so the caller knows what's happening.

See it in practice: Building a Chat Server #1: Hello, TCP uses this pattern for message parsing and error conversion.