Conversion Traits
Conversion traits are the standard-library vocabulary for changing how a value is viewed, owned, or validated; choosing the right one tells callers whether conversion is cheap, consuming, or fallible.
What it is
Rust does not rely on implicit user-defined conversions.
Instead, APIs expose explicit conversion traits such as From, Into, TryFrom, TryInto, AsRef, and Borrow.
That makes ownership, cost, and failure visible at the call site and composable in generic bounds.
The high-level split is:
From/Intofor infallible value-to-value conversions.TryFrom/TryIntofor fallible value-to-value conversions.AsReffor cheap borrowed views.Borrowfor borrowed views whoseEq,Ord, andHashbehavior matches the owned value.
How it works
From<T> for U automatically gives Into<U> for T through a blanket implementation, so library authors normally implement From and accept Into in caller-facing generic APIs.
TryFrom<T> for U similarly gives TryInto<U> for T.
AsRef<T> and Borrow<T> both start from &self, but they do not mean the same thing.
AsRef is an ergonomic view conversion.
Borrow is a stronger contract used by collections such as HashMap so an owned key can be queried by an equivalent borrowed key.
The compiler does not perform arbitrary implicit conversions at function-call boundaries.
Method lookup may auto-borrow or auto-deref, and numeric literals may be inferred, but a String does not silently become a PathBuf or an Email.
That explicitness is why conversion traits carry API-design meaning: the trait bound becomes part of the documented cost and failure model.
Blanket implementations matter for coherence.
If a crate owns Port, it can implement From<u16> for Port; downstream crates then receive u16: Into<Port> without writing another impl.
Because of the orphan rules, you cannot freely add conversion impls between two foreign types; wrap one side in a Newtype Pattern when you need a local conversion relationship.
Example
use std::convert::TryFrom;
#[derive(Debug, PartialEq, Eq)]
struct Port(u16);
impl From<u16> for Port {
fn from(value: u16) -> Self {
Self(value)
}
}
impl TryFrom<i32> for Port {
type Error = &'static str;
fn try_from(value: i32) -> Result<Self, Self::Error> {
let value = u16::try_from(value).map_err(|_| "port out of range")?;
Ok(Self(value))
}
}
fn connect(port: impl Into<Port>) -> Port {
port.into()
}
fn main() {
assert_eq!(connect(443_u16), Port(443));
assert_eq!(Port::try_from(8080), Ok(Port(8080)));
assert!(Port::try_from(-1).is_err());
}Edge cases
Error conversions are a legitimate From use because they are infallible wrappers, not validation.
The ? operator calls From::from to convert the source error into the function’s error type.
use std::{io, num::ParseIntError};
#[derive(Debug)]
enum ConfigError {
Io(io::Error),
Port(ParseIntError),
}
impl From<io::Error> for ConfigError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl From<ParseIntError> for ConfigError {
fn from(error: ParseIntError) -> Self {
Self::Port(error)
}
}
fn parse_port(input: &str) -> Result<u16, ConfigError> {
let port = input.trim().parse::<u16>()?;
Ok(port)
}
fn main() {
assert_eq!(parse_port("443").unwrap(), 443);
assert!(parse_port("not-a-port").is_err());
}Common errors
Trying to use a conversion trait that has not been implemented usually produces a trait-bound error:
error[E0277]: the trait bound `Port: From<i32>` is not satisfiedFix the call-site type (443_u16 instead of 443), implement the appropriate local From or TryFrom, or use an explicit checked conversion such as u16::try_from(value).
Do not paper over the error by adding a lossy or panicking From implementation.
Best practice
- ✅ Implement
Fromrather thanInto; the reciprocalIntoimplementation is provided automatically. - ✅ Implement
TryFromrather than panicking when conversion can reject input. - ✅ Use
AsReffor flexible borrowed parameters such as paths, strings, and byte slices. - ✅ Reserve
Borrowfor lookup-equivalent borrowed forms, especially collection keys. - ✅ Use conversion traits at API boundaries, then switch to concrete domain types inside the function body.
- ✅ Prefer named inherent methods when there are multiple plausible conversions, such as endian-specific byte decoding.
Pitfalls
- ⚠️ Do not put validation failure inside
From; use TryFrom and TryInto or a named constructor. - ⚠️ Do not implement
Borrowfor “just one field” unless equality, ordering, and hashing match; see Implementing Borrow for Partial Views. - ⚠️ Avoid inventing parallel conversion names when Conversion Method Prefixes or standard traits already express the operation.
- ⚠️ Avoid accepting
impl Into<T>and then calling.into()repeatedly; conversion should happen once. - ⚠️ Do not use trait conversions to hide expensive work that would surprise callers; document allocation or prefer a named method.
See also
From and Into · TryFrom and TryInto · AsRef for Flexible Arguments · Borrow for Equivalent Keys · Conversion Method Prefixes · Constructor Naming · Newtype Pattern · The Question Mark Operator · Option vs Result · Idioms & API Design
Sources
- Rust By Example, “From and Into” and “TryFrom and TryInto” - rust-by-example, https://doc.rust-lang.org/rust-by-example/conversion/from_into.html and https://doc.rust-lang.org/rust-by-example/conversion/try_from_try_into.html
- Standard library conversion traits - https://doc.rust-lang.org/std/convert/index.html
From- https://doc.rust-lang.org/std/convert/trait.From.htmlTryFrom- https://doc.rust-lang.org/std/convert/trait.TryFrom.htmlAsRef- https://doc.rust-lang.org/std/convert/trait.AsRef.html
