How to Use std

:convert (From, Into, TryFrom, TryInto)

Implement From and Into for safe conversions, and TryFrom and TryInto for fallible conversions in Rust.

When raw types aren't enough

You are building a configuration system. The user provides a string from a file. Your code needs a Port number. You write a function parse_port(s: &str) -> Result<u16, Error>. It works. Then you realize you also need to convert a u32 from a database into a Port, and you need to convert a Port back into a u16 for serialization. Suddenly you have parse_port, port_from_u32, port_to_u16, and port_display. The API is a mess of ad-hoc functions. Callers have to remember which function does what. The compiler cannot help you swap implementations or enforce consistency.

Rust solves this with traits. The std::convert module defines a standard vocabulary for type transformations. From and Into handle conversions that always succeed. TryFrom and TryInto handle conversions that might fail. By implementing these traits, you give the compiler a map of how your types relate. Generic code can then work with any type that supports the conversion, and the compiler enforces correctness everywhere.

The From and Into pair

From and Into are two sides of the same coin. From defines the logic to create a new type from an existing one. Into asks a value to transform itself into another type. The relationship is one-to-one: if you can build a B from an A, you can also turn an A into a B.

The trait signatures look like this:

pub trait From<T> {
    fn from(value: T) -> Self;
}

pub trait Into<T> {
    fn into(self) -> T;
}

From takes ownership of the source value and returns Self, the target type. Into takes ownership of self and returns the target type. Both consume the input. This is intentional. Conversions often involve allocation or validation, and taking ownership makes the data flow explicit.

Implement From for the target type. The convention is to define the conversion logic where the result lives. If you have a struct Level and want to create it from an i32, you implement From<i32> for Level. This keeps the implementation close to the type that owns the data.

The blanket implementation

You never need to implement Into manually. The standard library provides a blanket implementation that grants Into automatically whenever From is available.

// This code lives in std, not in your crate
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

This generic impl says: for any types T and U, if U implements From<T>, then T automatically implements Into<U>. The implementation of into simply calls U::from(self). There is no runtime overhead. The compiler resolves the call to From::from during monomorphization and inlines the logic.

This design reduces boilerplate. You write the conversion logic once in From, and you get both Level::from(val) and val.into() for free. The ecosystem relies on this symmetry. Libraries often accept impl Into<T> to allow callers to pass either the target type directly or any type that converts to it.

Implement From and let the compiler handle the rest. You get Into for free.

Minimal example

Here is a complete example showing From and the automatic Into.

use std::convert::{From, Into};

// A newtype wrapping an i32 with validation logic
struct Level(i32);

// Implement From<i32> for Level
// From defines the conversion logic from the source to the target
impl From<i32> for Level {
    fn from(raw: i32) -> Self {
        // Clamp the value to ensure levels are always positive
        // This logic runs every time the conversion happens
        let clamped = if raw < 1 { 1 } else { raw };
        Level(clamped)
    }
}

fn main() {
    // Call From directly using the associated function syntax
    // This is explicit and clear about the conversion direction
    let lvl1 = Level::from(5);
    
    // Use Into via the method syntax
    // The compiler finds the Into impl provided by the blanket impl
    // and resolves it to Level::from
    let lvl2: Level = 10.into();
    
    // Into requires a type annotation because the compiler
    // needs to know the target type to select the correct impl
    // let lvl3 = 20.into(); // Error: type annotations needed
    
    println!("Levels: {}, {}", lvl1.0, lvl2.0);
}

The From implementation clamps negative values. This validation runs whether you call Level::from or .into(). The Into call requires a type annotation on the left side. Without : Level, the compiler cannot determine what 10.into() should produce. The compiler rejects ambiguous into calls with E0283 (type annotations needed).

TryFrom and TryInto for fallible conversions

Not all conversions succeed. Parsing a string into a number can fail if the string contains letters. Converting a u32 to a u16 can fail if the value exceeds the range. For these cases, use TryFrom and TryInto.

pub trait TryFrom<T> {
    type Error;
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

pub trait TryInto<T> {
    type Error;
    fn try_into(self) -> Result<T, Self::Error>;
}

TryFrom returns a Result. The Ok variant contains the converted value. The Err variant contains an error describing why the conversion failed. The type Error associated type allows each conversion to define its own error type. A TryFrom<&str> might return a ParseError. A TryFrom<u32> might return a RangeError. This flexibility lets you provide precise error information without forcing a single error type for all conversions.

Just like From and Into, there is a blanket implementation for TryInto. Implement TryFrom, and you get TryInto automatically. The convention is the same: implement TryFrom on the target type, use try_into() at the call site.

The ? operator works seamlessly with try_into(). Since the return type is Result, you can propagate errors with a single character. This makes fallible conversions ergonomic in functions that already return Result.

Realistic example: Parsing and validation

Consider a Color struct that stores RGB values. You want to parse a hex string like "#ff00aa" into a Color, and you also want to convert a u32 packed integer into a Color. Both conversions can fail.

use std::convert::TryFrom;

struct Color {
    r: u8,
    g: u8,
    b: u8,
}

// Custom error type for hex parsing
#[derive(Debug)]
struct HexParseError {
    message: &'static str,
}

impl TryFrom<&str> for Color {
    // Define the error type for this specific conversion
    type Error = HexParseError;

    fn try_from(hex: &str) -> Result<Self, Self::Error> {
        // Strip the '#' prefix if present
        let hex = hex.strip_prefix('#').unwrap_or(hex);
        
        // Validate length: must be exactly 6 hex characters
        if hex.len() != 6 {
            return Err(HexParseError { message: "Expected 6 hex digits" });
        }
        
        // Parse each pair of characters into a byte
        // This is a simplified parser for demonstration
        let r = u8::from_str_radix(&hex[0..2], 16)
            .map_err(|_| HexParseError { message: "Invalid red component" })?;
        let g = u8::from_str_radix(&hex[2..4], 16)
            .map_err(|_| HexParseError { message: "Invalid green component" })?;
        let b = u8::from_str_radix(&hex[4..6], 16)
            .map_err(|_| HexParseError { message: "Invalid blue component" })?;
            
        Ok(Color { r, g, b })
    }
}

fn main() {
    // Use try_into with a type annotation
    // The compiler infers the error type from the impl
    let result: Result<Color, _> = "#ff00aa".try_into();
    
    match result {
        Ok(color) => println!("Parsed: #{:02x}{:02x}{:02x}", color.r, color.g, color.b),
        Err(e) => println!("Failed: {}", e.message),
    }
    
    // Invalid input returns an error
    let bad: Result<Color, _> = "xyz".try_into();
    assert!(bad.is_err());
}

The try_from function returns Err early if validation fails. The ? operator inside try_from propagates parsing errors from from_str_radix into the HexParseError type. The caller matches on the Result to handle success or failure.

Treat the error type as part of the API. A vague String error hides bugs; a specific enum tells the caller exactly what went wrong.

Pitfalls and compiler errors

Ambiguous target types

Calling .into() without a type annotation is a common mistake. The compiler cannot guess what you want to convert into. It rejects the code with E0283 (type annotations needed).

let x = 42.into(); // Error: type annotations needed

Fix this by adding a type annotation.

let x: Level = 42.into(); // OK

If you are passing the value to a function, the function signature might provide enough context. If the function expects impl Into<Level>, the compiler can infer the target type.

Missing trait bounds

If you try to convert types that do not have an implementation, the compiler rejects the code with E0277 (the trait bound is not satisfied).

let s: String = 42.into(); // Error: the trait `From<i32>` is not implemented for `String`

This error means there is no From<i32> for String in scope. You must implement the conversion yourself or use a different approach.

The orphan rule

You can only implement a trait if either the trait or the type is local to your crate. This is the orphan rule. You cannot implement From<String> for i32 because neither From nor String nor i32 is local to your code.

// This will not compile
impl From<String> for i32 {
    fn from(s: String) -> Self {
        s.parse().unwrap()
    }
}

The compiler rejects this to prevent conflicts. If two crates both tried to implement From<String> for i32, the compiler would not know which one to use.

Work around this by wrapping the type in a newtype.

struct MyInt(i32);

impl From<String> for MyInt {
    fn from(s: String) -> Self {
        // Parsing logic here
        MyInt(s.parse().unwrap())
    }
}

The newtype MyInt is local to your crate, so you can implement From for it. This pattern is standard in Rust. It also lets you attach methods and traits to the wrapper without affecting the underlying type.

The orphan rule protects the ecosystem. Wrap your type if you need custom conversions for standard types.

Convention asides

Implement From, use Into. This is the universal convention. You will rarely see a manual Into implementation in idiomatic code. The blanket impl covers it.

Keep TryFrom error types descriptive. Avoid type Error = String unless you have a good reason. A custom error enum or struct allows callers to match on specific failure modes. It also lets you implement std::error::Error for integration with the broader error handling ecosystem.

Use From for infallible conversions only. If a conversion can fail, use TryFrom. Using From for fallible logic forces you to panic or return a sentinel value, both of which are error-prone. The type system should reflect the risk.

Prefer From over constructor functions when the conversion is a one-to-one mapping. If you have Config::from_map(map) and Config::from_json(json), consider implementing From<HashMap> and From<JsonValue> instead. This unlocks generic code and the into() method. Keep constructors for complex initialization that requires multiple arguments or optional fields.

Decision matrix

Use From when you have a guaranteed conversion and want to define the logic in one place.

Use Into when you want to consume a value and transform it, relying on the compiler to find the From implementation.

Use TryFrom when the conversion can fail, such as parsing strings or validating ranges.

Use TryInto when you need a fallible conversion and want the ergonomic ? operator support via Result.

Use a constructor function like new() when the conversion is complex, requires multiple arguments, or does not fit a one-to-one type mapping.

Use AsRef or AsMut when you need a zero-cost reference conversion without taking ownership.

Pick the trait that matches the risk. If it can fail, TryFrom is the only honest choice.

Where to go next