What is the From and Into trait

The From and Into traits enable automatic, safe type conversions in Rust by defining a single implementation that works in both directions.

When types need to talk to each other

You are building a config parser. You read "8080" from a file. You need a u32 to set a port number. In Python, you write int("8080") and move on. In Rust, writing let port: u32 = "8080"; triggers a compiler error. The types don't match. Rust refuses to guess.

You need a bridge. You need a defined way to turn a String into a u32. That bridge is the From and Into traits. They are the standard library's mechanism for type conversion. They make your code ergonomic, they power the ? operator, and they follow a single rule that saves you from writing boilerplate: implement one, get the other for free.

The factory recipe

Think of From as a factory recipe. The recipe describes how to build a specific product from raw materials. From<T> means "I can be created from T". The trait lives on the target type. If you want to create a UserId from a u64, you implement From<u64> for UserId.

Into is the request. Into<U> means "I can be turned into U". It lives on the source type. If you have a u64 and want a UserId, you call .into().

Here is the magic: the standard library provides a blanket implementation. If you write the factory recipe (From), the compiler automatically generates the request handler (Into). You never need to implement both. Implementing From gives you Into automatically.

/// Represents a user ID in the system.
struct UserId(u64);

/// Implement From to define how to create a UserId from a u64.
/// This is the factory recipe.
impl From<u64> for UserId {
    fn from(id: u64) -> Self {
        UserId(id)
    }
}

fn main() {
    // Explicit construction using the From trait.
    let id1 = UserId::from(42);

    // Implicit conversion using Into.
    // The compiler finds the From<u64> impl for UserId
    // and synthesizes the Into<UserId> impl for u64.
    let id2: UserId = 42.into();
}

Implement From once. The compiler handles the rest.

How the compiler connects the dots

When you write 42.into(), the compiler looks for an implementation of Into<UserId> on u64. If you only implemented From<u64> for UserId, you might expect this to fail. It doesn't.

The standard library contains this blanket implementation:

// Simplified view of the standard library magic.
impl<T, U> Into<U> for T where U: From<T> {
    fn into(self) -> U {
        U::from(self)
    }
}

This code reads: "For any types T and U, if U implements From<T>, then T automatically implements Into<U>." The Into implementation just calls U::from(self).

This design choice prevents duplication. If every crate implemented both From and Into, you could end up with conflicting implementations. By making Into derived from From, the standard library ensures there is exactly one conversion path. The community convention is strict: always implement From. Writing impl Into manually is considered a code smell.

Trust the blanket impl. You rarely need to write impl Into by hand.

Real-world power: error handling

The most impactful use of From is in error handling. The ? operator relies on Into to convert errors. When you use ? on a Result<T, E>, the operator calls .into() on the error E to convert it to the function's return error type.

If you implement From for your custom error type, the ? operator works seamlessly with underlying errors. You don't need to write .map_err() everywhere.

use std::io;

#[derive(Debug)]
enum AppError {
    Io(io::Error),
    InvalidConfig(String),
}

/// Allow AppError to wrap io::Error.
/// This enables the ? operator to convert io::Error to AppError automatically.
impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        AppError::Io(err)
    }
}

fn load_config() -> Result<String, AppError> {
    // std::fs::read_to_string returns Result<String, io::Error>.
    // The ? operator calls .into() on the io::Error.
    // Because From<io::Error> is implemented for AppError,
    // the conversion happens automatically.
    let data = std::fs::read_to_string("config.json")?;
    Ok(data)
}

fn main() {
    match load_config() {
        Ok(content) => println!("Loaded: {}", content),
        Err(e) => eprintln!("Error: {:?}", e),
    }
}

The ? operator is the silent partner of From. Write the conversion, and error handling writes itself.

Pitfalls and compiler errors

From has a strict contract: the conversion must never fail. If you can convert a u64 to a UserId, you must always be able to do it. There is no Result return type for From. The return type is Self.

If your conversion can fail, From is the wrong tool. Trying to force a fallible conversion into From leads to panics or silent data loss.

Consider parsing a string to an integer. The string might contain non-numeric characters. You cannot implement From<&str> for u32 because "abc".into() would have to panic or return a default value. The standard library uses TryFrom for this case. TryFrom returns a Result, allowing the caller to handle the error.

If you attempt to use ? with an error type that doesn't implement From, the compiler rejects the code with E0277 (trait bound not satisfied). The error message points out that the From trait is not implemented for the target error type.

// This fails to compile.
// E0277: the trait `From<std::io::Error>` is not implemented for `AppError`
fn bad_load() -> Result<String, AppError> {
    // The ? operator tries to convert io::Error to AppError via Into.
    // Since From is missing, Into is missing, and compilation fails.
    let _data = std::fs::read_to_string("missing.txt")?;
    Ok(String::new())
}

If the conversion can fail, From is a lie. Reach for TryFrom.

Function signatures and flexibility

When writing functions, Into shines in parameter lists. Accepting Into<T> allows callers to pass T directly or any type that converts to T. This makes your API flexible without forcing callers to write explicit conversions.

/// Accepts anything that can become a String.
/// Callers can pass String, &str, or custom types with Into<String>.
fn greet(name: impl Into<String>) {
    let name: String = name.into();
    println!("Hello, {}", name);
}

fn main() {
    // Pass a String directly.
    greet(String::from("Alice"));

    // Pass a &str. &str implements Into<String>.
    greet("Bob");
}

The compiler resolves impl Into<String> by looking for Into<String> on the argument type. If the argument is already a String, the identity conversion applies. If it's a &str, the From<&str> for String impl kicks in via the blanket impl.

Convention aside: prefer impl Into<T> or T: Into<T> in function signatures over T. It reduces friction for callers. If you accept String, callers with &str must call .to_string(). If you accept Into<String>, they can pass &str directly.

Decision matrix

Use From when you are defining a type and want to provide a reliable, infallible conversion from another type. Implement From to get Into automatically; this is the standard convention.

Use Into in function parameters when you want to accept a type or anything that can convert to it. This makes your API flexible without forcing callers to call .into() explicitly.

Use TryFrom when the conversion might fail. If parsing a string to an integer can result in garbage input, TryFrom returns a Result so the caller can handle the error instead of panicking.

Use AsRef or AsMut for cheap, reference-based conversions. If you just need to borrow a slice from a string or a file path, these traits avoid allocation and are faster for temporary views.

Trust the borrow checker. It usually has a point.

Where to go next