When conversion might fail
You are building a configuration parser. Your program reads a string from a file. You want to turn that string into a strongly typed LogLevel enum. Most of the time the string says debug or error. Sometimes it says DEBUG, Debug, or invalid_level. A guaranteed conversion trait would force you to panic on the bad input, or wrap everything in an Option and lose the reason for the failure. You need a conversion that can fail gracefully and tell you exactly what went wrong.
That is what TryFrom and TryInto exist for. They give you a safe, composable way to attempt a conversion and handle the failure path with a Result. The standard library provides From for infallible conversions. It provides TryFrom for fallible ones. The naming follows a clear pattern: From means it always works. TryFrom means it might not.
The TryFrom contract
The TryFrom trait lives in std::convert. It asks you to define two things. First, you declare an associated type called Error. This type represents every possible way the conversion can fail. Second, you implement the try_from method. The method takes the source value and returns a Result<Self, Self::Error>. If the conversion succeeds, you return Ok with your new type. If it fails, you return Err with an instance of your error type.
Think of TryFrom as a bouncer at a club. The bouncer checks your ID. If the ID is valid, you get in. If the ID is expired, fake, or missing, the bouncer hands you a specific reason for rejection instead of letting you crash the door or disappearing into the void. The trait formalizes that check into a type-safe interface that the rest of your code can compose with.
Minimal example
Here is the smallest working implementation. You define a newtype that only accepts positive integers. You implement TryFrom<i32> so you can attempt to convert a signed integer into your safe wrapper.
use std::convert::TryFrom;
/// A wrapper that guarantees the inner value is strictly positive.
struct PositiveInt(u32);
impl TryFrom<i32> for PositiveInt {
// Define the error type for this specific conversion.
type Error = &'static str;
fn try_from(value: i32) -> Result<Self, Self::Error> {
// Check the invariant before constructing the new type.
if value > 0 {
// Cast is safe here because we verified value > 0.
Ok(PositiveInt(value as u32))
} else {
// Return a descriptive error instead of panicking.
Err("Value must be strictly positive")
}
}
}
fn main() {
// The compiler automatically provides try_into() via a blanket impl.
let result: Result<PositiveInt, _> = 5.try_into();
println!("{:?}", result); // Ok(PositiveInt(5))
let bad_result: Result<PositiveInt, _> = (-3).try_into();
println!("{:?}", bad_result); // Err("Value must be strictly positive")
}
Notice the type annotation on result. You do not need to annotate bad_result because the compiler can infer the target type from the left side of the assignment. The try_into() method comes for free. You only implemented TryFrom. The standard library handles the rest.
Trust the trait system. Implement one side, and the compiler generates the other.
How the compiler fills in the gaps
Rust provides a blanket implementation that connects TryFrom and TryInto. The standard library contains this hidden rule:
impl<T, U> TryInto<U> for T
where
U: TryFrom<T>,
{
type Error = U::Error;
fn try_into(self) -> Result<U, Self::Error> {
U::try_from(self)
}
}
This blanket impl means you never need to write TryInto by hand. You implement TryFrom for your target type, and every source type that you support automatically gets a try_into() method. The compiler wires the method call to your try_from implementation. The error type flows through unchanged.
This design prevents duplication. If you implemented both traits manually, you would have to keep them in sync. A mismatch between the two would create silent bugs where try_into() returns a different error type than try_from(). The blanket impl makes that impossible. It also keeps your codebase smaller. You write the conversion logic once, in the direction that feels natural for your type.
Reach for TryFrom when you are defining a type. Reach for TryInto when you are consuming a type. The compiler handles the bridge.
Realistic example
Production code rarely uses &'static str as an error type. String slices cannot carry dynamic context, they cannot implement std::error::Error, and they make it hard to chain conversions. Real projects define a custom error enum. Here is how a configuration parser might convert a raw string into a strongly typed setting.
use std::convert::TryFrom;
use std::fmt;
/// Represents a parsed log level from configuration.
#[derive(Debug, PartialEq)]
enum LogLevel {
Debug,
Info,
Warn,
Error,
}
/// Custom error type that captures why parsing failed.
#[derive(Debug, PartialEq)]
enum ParseLevelError {
EmptyInput,
UnknownVariant(String),
}
impl fmt::Display for ParseLevelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseLevelError::EmptyInput => write!(f, "received empty string"),
ParseLevelError::UnknownVariant(s) => write!(f, "unknown level: {}", s),
}
}
}
impl TryFrom<&str> for LogLevel {
// Use the custom error enum instead of a raw string slice.
type Error = ParseLevelError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
// Trim whitespace before matching to handle accidental formatting.
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ParseLevelError::EmptyInput);
}
// Match case-insensitively by lowercasing first.
match trimmed.to_lowercase().as_str() {
"debug" => Ok(LogLevel::Debug),
"info" => Ok(LogLevel::Info),
"warn" => Ok(LogLevel::Warn),
"error" => Ok(LogLevel::Error),
other => Err(ParseLevelError::UnknownVariant(other.to_string())),
}
}
}
fn main() {
// Use the ? operator to propagate errors cleanly.
let level = LogLevel::try_from(" Info ").expect("should parse");
println!("{:?}", level); // LogLevel::Info
let bad = LogLevel::try_from("trace");
println!("{:?}", bad); // Err(ParseLevelError::UnknownVariant("trace"))
}
The ? operator works seamlessly with TryFrom because it returns a Result. You can chain multiple fallible conversions in a single function without nesting match statements. The error type flows upward until you handle it or return it. This keeps your parsing logic flat and readable.
Design your error types to carry context. The compiler will enforce that you handle every variant.
Pitfalls and compiler errors
Implementing TryFrom trips up beginners in predictable ways. The compiler catches them early, but the error messages can look dense if you do not know what to look for.
If you forget to import the trait or misspell the method, the compiler rejects the call with E0599 (no function named try_into found on i32). The fix is usually adding use std::convert::TryInto; or relying on the blanket impl by calling TryFrom::try_from(value) directly.
If your try_from method returns a Result with the wrong error type, you get E0308 (mismatched types). The compiler expects Result<Self, Self::Error>. If you accidentally return Result<Self, String> when type Error = &'static str, the type checker stops you. Align the associated type with the Err branch.
If you try to use try_into() inside a function that does not return a Result, the ? operator fails with E0277 (the trait bound std::ops::Try is not satisfied). The ? operator only works in functions that return Result, Option, or a type that implements Try. Either change the function signature to return Result<T, E>, or use .unwrap() or .expect() for quick scripts.
Convention aside: the Rust community strongly prefers custom error enums over &'static str or String for type Error. Custom enums implement std::error::Error cleanly, they compose with thiserror or anyhow, and they let you attach metadata like line numbers or original input values. Stick to enums for anything that leaves your module.
Treat the associated error type as a public contract. Once you publish it, changing it breaks downstream code.
When to reach for TryFrom
You will encounter several conversion patterns in Rust. Each one solves a different problem. Pick the right trait for the job.
Use From when the conversion is guaranteed to succeed and requires no validation. Use From for wrapping types, unit conversions, or zero-cost casts where the source and target share the same memory layout. Use TryFrom when the conversion depends on runtime data that might be invalid. Use TryFrom for parsing strings, validating ranges, or reconstructing types from serialized bytes. Use TryInto when you are consuming a value and want to convert it into a target type without naming the trait explicitly. Use TryInto when you want the compiler to infer the target type from context. Use manual parsing functions when the conversion requires external state, database lookups, or async operations that traits cannot express.
Reach for the trait that matches your failure mode. Infallible conversions get From. Fallible conversions get TryFrom. Everything else stays a regular function.