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 Error type with the caller in mind. A type Error = String is 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_from fails with TryFromIntError, 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 conversionImplementReturns
Always succeeds (widening u8u32, wrapper newtype)Fromthe value directly
Can fail on some inputs (narrowing, parsing, validating)TryFromResult<Self, Error>
Parsing specifically from a stringFromStr (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

  • From promises every source value converts; if a conversion can fail, From is wrong, implementing it forces a panic or silent corruption. Use TryFrom, which returns Result<Self, Self::Error>.
  • Implementing TryFrom gives you TryInto for free (blanket impl), and both return Result, so they compose with ? and your normal error handling instead of needing special-case match.
  • Choose a structured Error type (a thiserror enum), not String, for reusable conversions, so callers can branch on the failure and it composes with #[from]. The std library models this with real error types like TryFromIntError.
  • Use TryFrom to "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()).