Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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 (u8 to u32, &str to String, your error into a bigger error type) → From / Into. Implement From; you get Into free, and ? uses it.
  • It can fail (i64 to u8, a number that must be positive, a raw struct into a validated one) → TryFrom / TryInto, which return a Result.
  • It starts as text (a number from input, a colour like "#ff8800") → .parse::<T>(), which calls your FromStr.
  • It ends as text for a person → implement Display, then format!, 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

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.

Conversions that report failure instead of losing data.

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.

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.

A TryFrom or a new() that returns Result, so an invalid value can't exist.

Text in, text out

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.

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.