TryFrom and Fallible Conversions
Newtype Pattern and From/Into Conversions covered From/Into for conversions that always succeed. But many conversions can't make that promise: a String into an email address, an i64 into a u8, raw bytes into a header, all can fail on bad input. From is the wrong tool here, because implementing it forces you to either panic or silently corrupt data. TryFrom is the fallible counterpart: same ergonomics, but it returns a Result. This tutorial covers the From/TryFrom split and how TryFrom is the backbone of "parse, don't validate" at your program's boundaries.
When From Is a Lie
From declares that every value of the source type maps to a valid target. The moment that isn't true, From is the wrong abstraction, and the naive workarounds are both bad:
// WRONG: From can't fail, so you're forced to panic on bad input
impl From<i64> for Port {
fn from(n: i64) -> Port {
if n < 0 || n > 65535 {
panic!("invalid port"); // a conversion that panics is a landmine
}
Port(n as u16)
}
}
A panicking From turns an ordinary conversion (Port::from(user_input)) into something that can crash the program, and the caller has no Result to handle. TryFrom is the honest signature: it returns Result<Self, Self::Error>, so failure is a value the caller must deal with, not a hidden panic.
use std::convert::TryFrom;
impl TryFrom<i64> for Port {
type Error = String;
fn try_from(n: i64) -> Result<Port, String> {
match u16::try_from(n) {
Ok(p) => Ok(Port(p)),
Err(_) => Err(format!("{n} is not a valid port")),
}
}
}
TryInto Comes Free, and Pairs with ?
Just as implementing From gives you Into for free (the blanket impl from Generics and Trait Bounds), implementing TryFrom gives you TryInto automatically. So a single impl TryFrom<i64> for Port lets callers write either direction:
use std::convert::TryInto;
let p = Port::try_from(8080_i64)?; // TryFrom direction
let p: Port = 8080_i64.try_into()?; // TryInto direction — same impl, for free
The ? is the point. Because try_from/try_into return Result, they slot directly into the ? propagation from Error Handling in Practice, a conversion failure becomes an early return, converted into the function's error type via From just like any other ?. This is what makes TryFrom ergonomic: fallible conversions compose with the rest of your error handling instead of needing special-case match blocks.
Gotcha: pick your
Errortype with the caller in mind. Atype Error = Stringis quick but gives callers an unmatchable, unstructured failure, fine for a script, weak for a library. For anything reusable, use a structured error (a thiserror enum) so the failure carries a code the caller can branch on and so it composes with?and#[from]. The standard library's own conversions do this:u16::try_fromfails withTryFromIntError, a real type, not a string.
"Parse, Don't Validate" at the Boundary
The most valuable use of TryFrom is turning unstructured external input into a validated domain type once, at the edge of your program, so the rest of the code can trust it. The contrast is "validate" (check a raw value and keep passing the raw value around) versus "parse" (convert into a type that can't be invalid):
// validate: the check and the value drift apart — every user must re-check
fn send_email(addr: &str) -> Result<(), Error> {
if !addr.contains('@') { return Err(Error::BadEmail); }
// ... but nothing stops a later caller from passing an unchecked &str
}
// parse: validity is encoded in the type — if you have an Email, it's valid
struct Email(String);
impl TryFrom<String> for Email {
type Error = Error;
fn try_from(s: String) -> Result<Email, Error> {
if s.contains('@') { Ok(Email(s)) } else { Err(Error::BadEmail) }
}
}
fn send_email(addr: Email) { /* addr is guaranteed valid, no re-check */ }
Once Email exists, every function taking an Email is statically guaranteed a validated value, the check happens exactly once, in try_from, and the newtype (Newtype Pattern) carries that proof through the whole program. This is the type-driven version of validation: instead of defensively re-checking at every layer, you make the invalid state unrepresentable past the boundary.
From/TryFrom: Which to Implement
| The conversion | Implement | Returns |
|---|---|---|
Always succeeds (widening u8→u32, wrapper newtype) | From | the value directly |
| Can fail on some inputs (narrowing, parsing, validating) | TryFrom | Result<Self, Error> |
| Parsing specifically from a string | FromStr (enables .parse()) | Result<Self, Error> |
The rule is simply: does every input map to a valid output? If yes, From (infallible, no Result to handle). If some inputs must be rejected, TryFrom, never reach for a panicking From to dodge the Result. A related note: for string-specific parsing, FromStr is the idiomatic choice over TryFrom<&str> because it powers the str::parse() method ("8080".parse::<Port>()?), though the two overlap heavily.
Key Takeaways
Frompromises every source value converts; if a conversion can fail,Fromis wrong, implementing it forces a panic or silent corruption. UseTryFrom, which returnsResult<Self, Self::Error>.- Implementing
TryFromgives youTryIntofor free (blanket impl), and both returnResult, so they compose with?and your normal error handling instead of needing special-casematch. - Choose a structured
Errortype (athiserrorenum), notString, for reusable conversions, so callers can branch on the failure and it composes with#[from]. The std library models this with real error types likeTryFromIntError. - Use
TryFromto "parse, don't validate": convert raw external input into a validated newtype once at the boundary, after which every function taking that type is statically guaranteed a valid value, no defensive re-checking. - Decide by asking "does every input map to a valid output?" Yes →
From; some inputs rejected →TryFrom; parsing from a string specifically →FromStr(powers.parse()).