From and Into Traits in Rust and Implementing Both Correctly
Implement From and you get Into for free. Learn the conventions, how the question mark uses From, and when TryFrom is the right trait.

From and Into are the standard library's two halves of one idea: converting a value of one type into another type when the conversion cannot fail. You implement From, you call .into(), and the language fills in the other half. This post shows how the pair works, the single rule that decides which one you write, how ? uses From to convert errors, and the traps that make a conversion surprising. Every snippet compiled on Rust 1.99, edition 2024.
Implement From, get Into for free
Suppose you have a Celsius and a Fahrenheit type and want to turn one into the other. The conversion is infallible and cheap, which is the signal that From is the right trait:
#[derive(Debug, Clone, Copy)]
struct Celsius(f64);
#[derive(Debug, Clone, Copy)]
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let boiling = Celsius(100.0);
let a = Fahrenheit::from(boiling);
let b: Fahrenheit = boiling.into();
println!("{a:?} {b:?}");
}Fahrenheit(212.0) Fahrenheit(212.0)Both spellings compile, even though the code only wrote impl From. The standard library contains one blanket implementation that makes it work for every type:
impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
}That is the whole mechanism. Into exists so that a conversion can be written as a method on the source value, which reads left to right and does not require naming the target type when inference knows it.
Implement From, never Into
The official guidance is to implement From and leave Into alone, and there is a concrete reason. If you write impl Into<Fahrenheit> for Celsius yourself, you do not get the matching From, so Fahrenheit::from(x) fails and the generic bound T: From<Celsius> is unsatisfied. Implementing From gives you both directions of the call, and the blanket impl supplies the rest.
The one case where a direct Into impl is needed is when the orphan rule blocks From: converting your type into a foreign type where the trait is also foreign. That is rare, and the pattern in the orphan rule post usually avoids it by making a newtype.
Accept Into in function arguments
The most useful place for Into is a parameter. A function that takes impl Into<String> accepts a String, a &str, a char, or a Box<str> without the caller writing .to_string():
#[derive(Debug)]
struct User {
name: String,
}
impl User {
fn new(name: impl Into<String>) -> Self {
User { name: name.into() }
}
}
fn main() {
let a = User::new("ferris");
let b = User::new(String::from("corro"));
let c = User::new('r');
println!("{a:?} {b:?} {c:?}");
}User { name: "ferris" } User { name: "corro" } User { name: "r" }There is a cost to know about. A generic parameter creates one compiled copy of new per type it is called with, so a constructor called with three types is compiled three times. Binary size grows slightly, and the call is usually inlined anyway. For a function that appears in thousands of places it is common to split it: a thin generic wrapper that calls .into() and hands the result to a non-generic inner function.
How the question mark uses From
The ? operator does more than return early. On the error path it calls From::from to convert the error into the function's return error type. That is the reason a function returning Result<T, MyError> can use ? on an io::Result and a ParseIntError result in the same body:
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Parse(ParseIntError),
TooLarge(u32),
}
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self {
ConfigError::Parse(e)
}
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::Parse(e) => write!(f, "not a number: {e}"),
ConfigError::TooLarge(n) => write!(f, "{n} exceeds 100"),
}
}
}
fn read_workers(text: &str) -> Result<u32, ConfigError> {
let n: u32 = text.trim().parse()?;
if n > 100 {
return Err(ConfigError::TooLarge(n));
}
Ok(n)
}
fn main() {
for input in ["8", "abc", "250"] {
match read_workers(input) {
Ok(n) => println!("workers = {n}"),
Err(e) => println!("error: {e}"),
}
}
}workers = 8
error: not a number: invalid digit found in string
error: 250 exceeds 100The line text.trim().parse()? produces a ParseIntError on failure, and the compiler inserts ConfigError::from(e) before returning. Delete the impl From and the same code is rejected:
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Parse(ParseIntError),
}
fn read_workers(text: &str) -> Result<u32, ConfigError> {
let n: u32 = text.trim().parse()?;
Ok(n)
}
fn main() {
println!("{:?}", read_workers("8"));
}error[E0271]: type mismatch resolving `<u32 as FromStr>::Err == ConfigError`
--> src/main.rs:9:30
|
9 | let n: u32 = text.trim().parse()?;
| ^^^^^ expected `ConfigError`, found `ParseIntError`The crate thiserror writes those From impls for you with a #[from] attribute, which is the main reason people reach for it. When to prefer it over anyhow is in anyhow versus thiserror.
Notice how the message is phrased. The compiler does not say "no From impl found"; it reports a type mismatch on parse, because it first tries to make parse produce the function's error type directly. When you see "expected X, found ParseIntError" on a ?, the fix is nearly always a missing impl From<ParseIntError> for X, or a .map_err(...) at that call.
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Parse(ParseIntError),
}
fn read_workers(text: &str) -> Result<u32, ConfigError> {
let n: u32 = text.trim().parse().map_err(ConfigError::Parse)?;
Ok(n)
}
fn main() {
println!("{:?}", read_workers("8"));
println!("{:?}", read_workers("x"));
}Ok(8)
Err(Parse(ParseIntError { kind: InvalidDigit }))That works because a tuple variant is itself a function from its payload to the enum. Use map_err when one call site needs a conversion that you do not want to make universal. Use From when every error of that type should turn into yours in the same way, which is almost always.
What From promises, and what it does not
The standard library documents three expectations that are worth keeping: the conversion is infallible, it is cheap relative to what it moves, and it does not lose information in a way a reader would not expect. The first one is enforced by the type. A From impl that panics on bad input breaks the contract, because ? and .into() are used in places where nobody expects a panic. When a conversion can fail, use TryFrom, the topic of the next post.
The standard library follows the rule closely. There is From<u8> for u32 and From<u32> for u64, which widen losslessly, and there is no From<u64> for u32, which could truncate. That is why let x: u32 = big_u64.into() fails and u32::try_from(big_u64)? is the correct spelling:
fn main() {
let small: u8 = 200;
let wide: u32 = small.into();
let widest: u64 = wide.into();
println!("{small} {wide} {widest}");
let big: u64 = 5_000_000_000;
let narrow: u32 = big.into();
println!("{narrow}");
}error[E0277]: the trait bound `u32: From<u64>` is not satisfied
--> src/main.rs:8:27
|
8 | let narrow: u32 = big.into();
| ^^^^ the trait `From<u64>` is not implemented for `u32`
|
= help: `u32` implements trait `From<T>`:
From<Ipv4Addr>
From<bool>
From<char>
From<std::ascii::Char>
From<u16>
From<u8>
= note: required for `u64` to implement `Into<u32>`Common From impls you should know exist
| Conversion | What happens |
|---|---|
String::from("x"), "x".into() | Allocates and copies the text |
Box::<str>::from("x") | Allocates a box with no spare capacity |
Vec::from([1, 2, 3]) | Moves the array into a vector |
PathBuf::from("a/b") | Owns a path buffer |
Option::from(5) | Wraps the value in Some |
HashMap::from([("a", 1)]) | Builds a map from an array of pairs |
f64::from(7_i32) | Widens an integer into a float, without loss |
Traps that make a conversion surprising
Inference does not always pick the target you meant. let x = value.into(); with nothing else to pin the type is ambiguous and the compiler asks for an annotation (E0282, see type annotations needed). A second trap is hidden allocation. .into() on a &str producing a String looks as innocent as a cast, and in a hot loop it is a heap allocation per iteration. If the callee only needs to read the text, take &str and let the caller decide.
The third is conflicting impls. Because of the blanket impl<T> From<T> for T, you cannot write impl From<MyType> for MyType, and a broad blanket impl of your own, such as impl<T: Display> From<T> for MyError, will overlap with it and be rejected as E0119. Write the impls you need one at a time.
When From is the wrong tool
A conversion that needs context (a database connection, a locale, a configuration value) cannot be From, because the trait has no way to receive it; use a named constructor such as Money::from_cents_with_rate. A conversion that costs real work, such as parsing a file, deserves a method name that says so. And conversions with more than one sensible answer, such as a Duration to seconds as a float or as whole seconds, are better as explicit methods than as a From whose choice is invisible at the call site.


