TryFrom and TryInto¶
Level: 201 · working knowledge
One line: The conversion that is allowed to say no — same shape as From, plus a type Error, which is the whole difference and the whole point.
use std::convert::TryFrom;
#[derive(Debug)]
struct Score(u8);
#[derive(Debug)]
struct OutOfRange(u8);
impl TryFrom<u8> for Score {
type Error = OutOfRange;
fn try_from(n: u8) -> Result<Self, Self::Error> {
if n <= 5 { Ok(Score(n)) } else { Err(OutOfRange(n)) }
}
}
fn main() {
println!("{:?}", Score::try_from(4u8)); // Ok(Score(4))
println!("{:?}", Score::try_from(9u8)); // Err(OutOfRange(9))
}
TryInto comes free from TryFrom by the same blanket impl that gives Into from From. Both are in the prelude since the 2021 edition, so the use line above is habit rather than necessity.
std's own, which you meet first¶
fn main() {
println!("{:?}", u8::try_from(300i32)); // Err(TryFromIntError(PosOverflow))
println!("{:?}", u8::try_from(200i32)); // Ok(200)
println!("{:?}", u8::try_from(-1i32)); // Err(TryFromIntError(NegOverflow))
}
Every narrowing integer conversion has one, and the error is TryFromIntError, which carries nothing — the failure has exactly one cause, so there is nothing to report. Compare OutOfRange(9) above, which keeps the offending value so the message can name it. Deciding between those two is most of the work of writing a TryFrom.
Collecting stops at the first refusal¶
fn read_ballot(cells: &[u8]) -> Result<Vec<Score>, OutOfRange> {
cells.iter().map(|&n| Score::try_from(n)).collect()
}
Vec<Result<T, E>> collected into Result<Vec<T>, E> is one of the most useful shapes in std: it short-circuits, so nothing after the bad cell is converted, and the caller gets one error rather than a vector of them. When you want every failure instead, partition is the other half of that pattern.
For text, implement FromStr instead¶
use std::str::FromStr;
impl FromStr for Score {
type Err = ScoreError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let n: u32 = s.trim().parse().map_err(|_| ScoreError::NotANumber(s.into()))?;
Score::try_from(n)
}
}
FromStr is what .parse() calls, so implementing it gives you "4".parse::<Score>() and — more usefully — map(str::parse) inside an iterator chain. TryFrom<&str> would work and give you neither. Note the field is type Err, not type Error: the two traits spell it differently, which costs everybody one compile error exactly once.
The trap: as is the same conversion with the check deleted¶
fn main() {
println!("{:?}", u8::try_from(300i32)); // Err(TryFromIntError(PosOverflow))
println!("{}", 300i32 as u8); // 44
}
Both are "convert i32 to u8". One reports that it could not; the other returns 44 and says nothing — no panic, no warning, in debug builds too. The practice below runs a ballot parser both ways: the as version accepts a score of 9 on a 0–5 ballot and turns a non-numeric cell into a zero indistinguishable from a real one.
The test: if you cannot say out loud what should happen to the out-of-range case, you wanted try_from.
If you are coming from another language¶
- Python.
int("abc")raisingValueErrorisTryFromwith the error moved into the return type instead of the control flow. The port of atry: … except ValueError:block is amatchon theResultor a?, and the difference that matters is that Rust's version is in the signature — you cannot fail to notice that a conversion can fail, where in Python the only way to know is to read the docs or be surprised in production. Python'sint()is also happy to truncate a float silently (int(3.9)is 3); the Rust equivalent of that decision isas, and it is deliberately a different spelling. - ABAP. This is the
CATCH cx_sy_conversion_no_numberyou write around aMOVEfrom aCHARfield, and the improvement is the same one Python gets: the failure is in the type rather than in an exception class you have to remember exists. The closer analogy is the pairMOVE/MOVE … ?TOfor object references — the second one can fail and raisesCX_SY_MOVE_CAST_ERROR, which is exactly theFrom/TryFromsplit with a runtime check instead of a compile-time one. What transfers directly: the ABAP habit of validating before assigning (IF lv_input CO '0123456789') is the same instinct as writing aTryFromimpl, but the impl does it once, in a named place, rather than at every call site. - C#.
int.Parseandint.TryParseare exactly this pair, and the naming is close enough to be a mnemonic. Rust's version returns the value inside the result rather than through anoutparameter, which is what lets it chain. - Go.
strconv.Atoireturning(int, error)is the same signature;?is what Go'sif err != nil { return err }would be if it were an operator.
The verified output¶
Verified output of tryfrom_and_tryinto.rs — regenerated by tools/run_examples.py, never hand-typed.
1. One impl, two call syntaxes, one of them fallible
Score::try_from(4) = Ok(Score(4))
Score::try_from(9) = Err(ScoreOutOfRange(9))
4u8.try_into() = Ok(Score(4))
`TryInto` comes free from `TryFrom`, exactly as `Into` comes free
from `From` — same blanket impl, one Result deeper.
2. The difference from From, in one line
From has no error type, so a conversion that cannot honour its
input must clamp, wrap or panic. TryFrom has `type Error`, so it
can hand the bad value back and let the caller decide.
3. std's own, which you will meet first
u8::try_from(300i32) = Err(TryFromIntError(PosOverflow))
u8::try_from(200i32) = Ok(200)
u8::try_from(-1i32) = Err(TryFromIntError(NegOverflow))
i32::try_from(9u64) = Ok(9)
The error is `TryFromIntError`, and it carries nothing — the
failure has exactly one cause, so there is nothing to report.
Compare that with ScoreOutOfRange above, which keeps the value.
4. Collecting into a Result stops at the first refusal
read_ballot([5, 3, 0]) = Ok([Score(5), Score(3), Score(0)])
read_ballot([5, 9, 0]) = Err(ScoreOutOfRange(9))
`Vec<Result<T, E>>` collected into `Result<Vec<T>, E>` is one of
the most useful shapes in std: it short-circuits, so the third
cell is never converted, and the caller gets one error rather
than a vector of them.
5. The trap: `as` is the same conversion with the check deleted
u8::try_from(300) = Err(TryFromIntError(PosOverflow))
300 as u8 = 44
Both are "convert i32 to u8". One reports that it could not; the
other returns 44 and says nothing. Reach for `as` when truncation
is what you meant, and for try_from when it is not.
Practice¶
A ballot line, parsed twice, and one of the two accepts junk. Write a Score that refuses anything above 5, give it both a TryFrom<u32> and a FromStr, and parse "5,3,0" into a Vec<Score> with one collect. Then run three broken lines through it: a non-numeric cell, a score of 9, and one with stray spaces — one of those three is not an error, and deciding that is part of writing the impl.
Now write the same parser with parse().unwrap_or(0) as u8 instead, run the same lines, and look at what comes out. Both versions return a full, plausible ballot. Say what each of the two bad cells became, and why neither is detectable downstream.
Solution
tryfrom_and_tryinto_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: a ballot line, parsed three ways, two of which accept junk.
//!
//! rustc --edition 2024 tryfrom_and_tryinto_kata.rs -o /tmp/tfk && /tmp/tfk
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Score(u8);
#[derive(Debug, PartialEq)]
enum ScoreError {
NotANumber(String),
OutOfRange(u32),
}
impl fmt::Display for ScoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScoreError::NotANumber(s) => write!(f, "{s:?} is not a number"),
ScoreError::OutOfRange(n) => write!(f, "{n} is outside 0-5"),
}
}
}
impl TryFrom<u32> for Score {
type Error = ScoreError;
fn try_from(n: u32) -> Result<Self, Self::Error> {
if n <= 5 {
Ok(Score(n as u8))
} else {
Err(ScoreError::OutOfRange(n))
}
}
}
/// `FromStr` is what `.parse()` calls. Implement this rather than
/// `TryFrom<&str>` when the source is text: you get `"4".parse()?` free.
impl FromStr for Score {
type Err = ScoreError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let n: u32 = s
.trim()
.parse()
.map_err(|_| ScoreError::NotANumber(s.to_string()))?;
Score::try_from(n)
}
}
/// The version that reports the first bad cell.
fn parse_line(line: &str) -> Result<Vec<Score>, ScoreError> {
line.split(',').map(str::parse).collect()
}
/// The version that quietly accepts everything.
fn parse_line_as(line: &str) -> Vec<Score> {
line.split(',')
.map(|c| Score(c.trim().parse::<u32>().unwrap_or(0) as u8))
.collect()
}
fn main() {
println!("1. The good line");
println!(" parse_line(\"5,3,0\") = {:?}", parse_line("5,3,0"));
println!(" `.parse()` reached FromStr, FromStr reached TryFrom, and collect");
println!(" turned Vec<Result<_>> into Result<Vec<_>>.");
println!();
println!("2. The three ways one line can be wrong");
for line in ["5,x,0", "5,9,0", "5, 3 ,0"] {
match parse_line(line) {
Ok(v) => println!(" {line:12} -> Ok({:?})", v.iter().map(|s| s.0).collect::<Vec<_>>()),
Err(e) => println!(" {line:12} -> Err({e})"),
}
}
println!(" The third is not an error: FromStr trims, so whitespace is fine.");
println!(" Deciding that is part of writing the impl, and it is written down");
println!(" in one place instead of at every call site.");
println!();
println!("3. The same lines through `as`");
for line in ["5,x,0", "5,9,0"] {
let v = parse_line_as(line);
println!(" {line:12} -> {:?}", v.iter().map(|s| s.0).collect::<Vec<_>>());
}
println!(" Both produce a full, plausible, wrong ballot. `x` became a zero");
println!(" score — indistinguishable from a real zero — and 9 became 9,");
println!(" which is not a legal score at all: the `as u8` cast let it past");
println!(" the type that exists to prevent exactly that.");
println!();
println!("4. Two traits, and which to implement");
println!(" TryFrom<u32> for Score -> Score::try_from(4u32), 4u32.try_into()");
println!(" FromStr for Score -> \"4\".parse::<Score>()");
println!(" Implementing FromStr also gives you `.parse()` inside iterator");
println!(" chains, as `map(str::parse)` above — which is why `TryFrom<&str>`");
println!(" is the wrong choice for a type parsed out of text.");
println!();
println!("5. The error type is the deliverable");
let e = "9".parse::<Score>().unwrap_err();
println!(" \"9\".parse::<Score>() error = {e}");
println!(" debug form: {e:?}");
println!(" ScoreError keeps the offending value, so the message can name it.");
println!(" std's TryFromIntError does not, because there is only ever one");
println!(" thing to say. Decide which of those your caller needs before you");
println!(" write `type Error = String`.");
}
Verified output of tryfrom_and_tryinto_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The good line
parse_line("5,3,0") = Ok([Score(5), Score(3), Score(0)])
`.parse()` reached FromStr, FromStr reached TryFrom, and collect
turned Vec<Result<_>> into Result<Vec<_>>.
2. The three ways one line can be wrong
5,x,0 -> Err("x" is not a number)
5,9,0 -> Err(9 is outside 0-5)
5, 3 ,0 -> Ok([5, 3, 0])
The third is not an error: FromStr trims, so whitespace is fine.
Deciding that is part of writing the impl, and it is written down
in one place instead of at every call site.
3. The same lines through `as`
5,x,0 -> [5, 0, 0]
5,9,0 -> [5, 9, 0]
Both produce a full, plausible, wrong ballot. `x` became a zero
score — indistinguishable from a real zero — and 9 became 9,
which is not a legal score at all: the `as u8` cast let it past
the type that exists to prevent exactly that.
4. Two traits, and which to implement
TryFrom<u32> for Score -> Score::try_from(4u32), 4u32.try_into()
FromStr for Score -> "4".parse::<Score>()
Implementing FromStr also gives you `.parse()` inside iterator
chains, as `map(str::parse)` above — which is why `TryFrom<&str>`
is the wrong choice for a type parsed out of text.
5. The error type is the deliverable
"9".parse::<Score>() error = 9 is outside 0-5
debug form: OutOfRange(9)
ScoreError keeps the offending value, so the message can name it.
std's TryFromIntError does not, because there is only ever one
thing to say. Decide which of those your caller needs before you
write `type Error = String`.
See also¶
FromandInto— the infallible half, and where?'s conversion rule is explained- Casting with
as— the conversion with no check at all - Parsing out of a string —
.parse()from the text side OptionvsResult— why this returns aResultand not anOption- A score is not a number — the type this conversion is protecting
Sources¶
Conversion: TryFrom and TryInto ↗ in Rust by Example, plus std::convert::TryFrom ↗ and std::str::FromStr ↗ for the type Err spelling.
Po polsku¶
TryFrom to konwersja, która ma prawo odmówić: ten sam kształt co From, plus type Error. Cała różnica mieści się w tym jednym elemencie i cały sens także — zwraca Result, więc możliwość niepowodzenia stoi w typie, a nie w komentarzu.
Najpierw spotyka się implementacje ze standardu, choćby u8::try_from(300i32), i właśnie na nich najlepiej widać, po co to jest. Rzutowanie przez as to ta sama konwersja z usuniętym sprawdzeniem: 300i32 as u8 daje 44 i nie mówi ani słowa. Warto zapamiętać tę różnicę w takiej właśnie formie — nie „as jest szybsze", tylko „as jest tym samym, tylko bez pytania, czy wynik ma sens".
Dwie rzeczy przydatne w praktyce. Przy zbieraniu do Result przetwarzanie zatrzymuje się na pierwszej odmowie i oddaje ten jeden błąd, co zwykle jest dokładnie tym, o co chodziło. A gdy konwersja idzie z tekstu, właściwą cechą nie jest TryFrom, tylko FromStr — to ona stoi za .parse(), którego i tak używasz.
Szukaj po polsku: konwersje zawodne · rzutowanie przez as · FromStr i parse · rust TryFrom vs as · rust collect into Result