Every way to convert between types
as, From and Into, TryFrom and TryInto, parse and FromStr, Display and to_string, and the string conversions. Which ones can fail, and which ones lose data quietly.
Ask: can this conversion fail, or lose information?
- It can never fail or lose anything (
u8tou32,&strtoString, your error into a bigger error type) →From/Into. ImplementFrom; you getIntofree, and?uses it. - It can fail (
i64tou8, a number that must be positive, a raw struct into a validated one) →TryFrom/TryInto, which return aResult. - It starts as text (a number from input, a colour like
"#ff8800") →.parse::<T>(), which calls yourFromStr. - It ends as text for a person → implement
Display, thenformat!,println!or.to_string(). - Numbers, when you really do want truncation or wrapping (a hash
folded into a
u8, a float to the nearest pixel) →as, with a comment saying the loss is intended. - Text between encodings or from the OS (bytes to a
String, a file name that isn't valid UTF-8) →String::from_utf8,from_utf8_lossy,OsString, each choosing what happens to bad bytes.
Numbers
Section
Conversions and casting
as for numbers, and the checked and lossless alternatives.
Formal. A numeric as cast always produces a value. Integer to
smaller integer keeps the low bits; float to integer rounds toward zero
and saturates at the target's limits (NaN becomes 0). From is provided
only where no information can be lost, and TryFrom/try_into return a
Result for conversions that can fail.
In plain English. as is the "just do it" conversion: it never
complains, even when it has to throw information away. Use from when
nothing can be lost, and try_from when it might be, so you find out.
Example. 300_i32 as u8 is 44; u8::try_from(300_i32) is an
Err; u32::from(200_u8) is 200.
Why this rule exists. Low-level code sometimes needs the raw bit behavior, but ordinary code should be told when a conversion loses data.
300 as u8 is 44 and -1.5 as u32 is 0, with no warning. That's why as is last on the list.
Term
TryFrom
Example
u8::try_from(300_i32) is Err(TryFromIntError(())); u8::try_from(200_i32) is Ok(200).Traits for your own types
The conversion traits every Rust type is expected to have where they make sense.
Section
From for error types
The conversion ? calls on the way up.
Formal. Check untrusted data once, at the boundary, by converting it
into a type whose values are valid by construction (a newtype with a
private field and a fallible constructor or TryFrom impl, an enum
instead of a string, NonZeroU32 instead of u32). Code that receives
the type can rely on its invariant without checking again. With serde,
#[serde(try_from = "Raw")] or a custom Deserialize makes
deserialization itself perform the conversion, so invalid input becomes
a deserialization error.
In plain English. Instead of checking a value and then carrying it around as a plain string or number (and hoping everyone remembers it was checked), turn it into a special type that can only ever hold good values. The check happens once, and the type remembers it.
Example. struct Port(u16) with impl TryFrom<u16> for Port that
rejects 0: a function taking Port never has to ask whether it's zero.
Why this rule exists. Scattered checks get forgotten; a type that can't represent bad data makes the compiler enforce the check everywhere.
Convert raw input into a type that can only hold valid values, once, at the edge.
Section
Constructors and validation
A TryFrom or a new() that returns Result, so an invalid value can't exist.
Text in, text out
Section
Parsing and formatting
parse for text to numbers, format specifiers for numbers to text.
Section
String and &str
to_string, String::from, as_str, and when a function should take &str.
Section
Parsing text
split, trim, strip_prefix and parse, combined on real input.
Section
Encoding and decoding
Bytes to text and back, with UTF-8 checked or replaced.
Formal. Operating systems store file names, environment variables,
and command-line arguments as platform strings: arbitrary non-zero
bytes on Unix, and 16-bit units (possibly unpaired surrogates) on
Windows. Neither is guaranteed to be valid UTF-8, so Rust gives them
their own types, OsString/OsStr and PathBuf/Path. Converting to
&str can fail (to_str returns Option), to_string_lossy replaces
bad sequences with U+FFFD, and std::env::args panics on an argument
that isn't Unicode (args_os doesn't).
In plain English. A file name is whatever the operating system allows, and that isn't always proper text. Rust keeps paths in a separate type so your program can still handle the odd ones, and asks you to decide what to do when you need them as text.
Example. Path::new("notes").join("2026").join("todo.txt") builds
notes/2026/todo.txt on macOS and Linux and notes\2026\todo.txt on
Windows; path.display() prints it for people.
Why this rule exists. Treating paths as strings silently breaks on real systems: files a program can list but not open, or names that get mangled when written back.
A file name is an OsStr. Converting it to a String can fail.
Whole data structures
Section
The serde model
A struct to JSON, TOML or bytes and back, by derive.
serde(try_from) runs your TryFrom as data comes in.