Rust type conversion
last modified November 13, 2026
In this article we show how to convert values between types in Rust.
Rust never converts types implicitly. An i32 is not
automatically widened to an i64, and a number does not become
a string when we concatenate it. Every conversion is written out, which
makes it visible where data can be lost.
There are several tools for the job:
ascasts between primitive types; they never fail but may lose dataFromandIntofor conversions that always succeedTryFromandTryIntofor conversions that can failparseandFromStrfor reading values from textto_stringandDisplayfor turning values into textAsReffor cheap reference-to-reference conversions
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q.
Casting with as
The as keyword converts between numeric types. It always
compiles and never panics, even when the value does not fit.
fn main() {
let small: u8 = 200;
let wide = small as u32;
println!("u8 {small} as u32: {wide}");
let big: i32 = 300;
println!("{big} as u8: {}", big as u8);
let neg: i32 = -1;
println!("{neg} as u32: {}", neg as u32);
println!("{neg} as u8: {}", neg as u8);
let x: u8 = 250;
println!("{x} as i8: {}", x as i8);
let n: i64 = 1_000_000;
let f = n as f64;
println!("{n} as f64: {f}");
let back = f as i16;
println!("{f} as i16: {back}");
}
Widening casts, such as u8 to u32, keep the
value. Narrowing casts keep only the lowest bits.
let big: i32 = 300;
println!("{big} as u8: {}", big as u8);
The number 300 is 0x12C. Only the lowest byte,
0x2C, fits into a u8, which gives 44. No error is
reported.
println!("{neg} as u32: {}", neg as u32);
println!("{x} as i8: {}", x as i8);
Casts between signed and unsigned types of the same size reinterpret the
bits: -1 is all ones, which is the largest u32, and the
u8 value 250 is -6 as an i8.
let back = f as i16;
Casts from floats to integers are different: they saturate.
A million is too large for an i16, so the result is the
largest i16, 32767.
$ cargo run -q u8 200 as u32: 200 300 as u8: 44 -1 as u32: 4294967295 -1 as u8: 255 250 as i8: -6 1000000 as f64: 1000000 1000000 as i16: 32767
Floats to integers
Casting a float to an integer truncates it towards zero. To round differently, we call a rounding method first.
fn main() {
let values = [2.7, -2.7, 2.5, 1e10, f64::NAN, f64::INFINITY];
for v in values {
println!(
"{v:>12}: as i32 {:>11}, round {:>11}, floor {:>11}",
v as i32,
v.round() as i32,
v.floor() as i32
);
}
let big = u64::MAX;
let f = big as f64;
println!("{big} as f64 = {f}");
println!("back to u64: {}", f as u64);
let n: i64 = 9_007_199_254_740_993;
println!("{n} as f64 = {}", n as f64);
}
We compare the plain cast with round and
floor for several values.
v as i32, v.round() as i32, v.floor() as i32
The cast drops the fractional part: 2.7 and -2.7 become 2 and -2. The
round method rounds half-way cases away from zero, so 2.5
becomes 3, and floor rounds down, so -2.7 becomes -3. Note
that round differs from the {:.0} format, which
rounds ties to even.
Values outside the range saturate to the minimum or maximum, and
NaN becomes 0. Before Rust 1.45, such casts were undefined
behaviour.
let n: i64 = 9_007_199_254_740_993;
println!("{n} as f64 = {}", n as f64);
Integer to float casts can lose precision. An f64 has 53 bits
of mantissa, so 253 + 1 cannot be represented and is rounded to
the nearest even neighbour. Likewise u64::MAX becomes
264, which is too large for a u64; the cast back
saturates.
$ cargo run -q
2.7: as i32 2, round 3, floor 2
-2.7: as i32 -2, round -3, floor -3
2.5: as i32 2, round 3, floor 2
10000000000: as i32 2147483647, round 2147483647, floor 2147483647
NaN: as i32 0, round 0, floor 0
inf: as i32 2147483647, round 2147483647, floor 2147483647
18446744073709551615 as f64 = 18446744073709552000
back to u64: 18446744073709551615
9007199254740993 as f64 = 9007199254740992
Lossless conversions with From
The standard library implements From for numeric conversions
that can never lose data.
fn average(values: &[i64]) -> f64 {
let sum: i64 = values.iter().sum();
sum as f64 / values.len() as f64
}
fn main() {
let a: u8 = 200;
let b: i16 = -1200;
let x = i32::from(a);
let y = i64::from(b);
let z = f64::from(b);
println!("{x} {y} {z}");
let w: u64 = a.into();
println!("{w}");
let c: u32 = 42;
let values = [i64::from(a), i64::from(b), c.into()];
println!("{:?} average {:.2}", values, average(&values));
let flag = true;
println!("{} {}", u8::from(flag), i32::from(!flag));
let letter = 'A';
println!("{}", u32::from(letter));
}
Unlike as, From documents that the conversion is
safe. If someone later changes the source type to a larger one, the code
stops compiling instead of silently truncating.
let x = i32::from(a); let w: u64 = a.into();
Every From<T> for U implementation also gives us
Into<U> for T. With into, the target type
must be known from the context: from a type annotation, a function
parameter, or, as in the array, from the other elements.
let z = f64::from(b);
An i16 converts to f64 exactly. There is no
From<i64> for f64, because not every i64
fits into the mantissa.
println!("{} {}", u8::from(flag), i32::from(!flag));
println!("{}", u32::from(letter));
A bool converts to 1 or 0, and a char to its
Unicode code point.
$ cargo run -q 200 -1200 -1200 200 [200, -1200, 42] average -319.33 1 0 65
Into requires a lossless conversion
A narrowing conversion has no From implementation, so
into does not compile.
fn main() {
let big: i64 = 5_000_000_000;
let n: i32 = big.into();
println!("{n}");
}
An i64 may not fit into an i32.
let n: i32 = big.into();
The compiler lists the types i32 can be created from. For a
conversion that may fail, we use try_into, shown next.
$ cargo run -q
error[E0277]: the trait bound `i32: std::convert::From<i64>` is not satisfied
--> src/main.rs:3:22
|
3 | let n: i32 = big.into();
| ^^^^ the trait `std::convert::From<i64>` is not implemented for `i32`
|
= help: `i32` implements trait `std::convert::From<T>`:
std::convert::From<bool>
std::convert::From<i16>
std::convert::From<i8>
std::convert::From<u16>
std::convert::From<u8>
= note: required for `i64` to implement `std::convert::Into<i32>`
For more information about this error, try `rustc --explain E0277`.
Fallible conversions with TryFrom
The TryFrom and TryInto traits return a
Result: Ok with the converted value, or an error
when the value is out of range.
fn main() {
let values: [i64; 4] = [42, 300, -5, 5_000_000_000];
for v in values {
match u8::try_from(v) {
Ok(b) => println!("{v} fits into u8: {b}"),
Err(e) => println!("{v} does not fit into u8: {e}"),
}
}
for v in values {
let r: Result<i32, _> = v.try_into();
println!("{v} -> i32: {r:?}");
}
let len: usize = 7;
let offset = i32::try_from(len).expect("length too large");
println!("offset {}", offset - 10);
}
We try to fit four i64 values into a u8 and an
i32.
match u8::try_from(v) {
Ok(b) => println!("{v} fits into u8: {b}"),
Err(e) => println!("{v} does not fit into u8: {e}"),
}
Instead of wrapping around like as, the conversion reports a
TryFromIntError. Both traits are in the prelude since the 2021
edition, so no use is needed.
let offset = i32::try_from(len).expect("length too large");
Conversions between usize and fixed-size types are a common
use. The size of usize depends on the platform, so even
usize to u64 goes through try_from.
$ cargo run -q 42 fits into u8: 42 300 does not fit into u8: out of range integral type conversion attempted -5 does not fit into u8: out of range integral type conversion attempted 5000000000 does not fit into u8: out of range integral type conversion attempted 42 -> i32: Ok(42) 300 -> i32: Ok(300) -5 -> i32: Ok(-5) 5000000000 -> i32: Err(TryFromIntError(PosOverflow)) offset -3
Strings and string slices
The two main string types, String and &str,
convert into each other easily.
fn shout(text: &str) -> String {
text.to_uppercase()
}
fn main() {
let a = String::from("one");
let b = "two".to_string();
let c = "three".to_owned();
let d: String = "four".into();
println!("{a} {b} {c} {d}");
let s1: &str = &a;
let s2 = b.as_str();
println!("{} {}", shout(s1), shout(s2));
println!("{}", shout(&c));
let ch = 'x';
let from_char = String::from(ch);
let chars = ['r', 'u', 's', 't'];
let from_chars: String = chars.iter().collect();
let back: Vec<char> = from_chars.chars().collect();
println!("{from_char} {from_chars} {back:?}");
let bytes = from_chars.clone().into_bytes();
println!("{bytes:?}");
let text = String::from_utf8(bytes).unwrap();
println!("{text}");
let bad = vec![0x66, 0x6f, 0xff];
match String::from_utf8(bad) {
Ok(s) => println!("{s}"),
Err(e) => println!("error: {e}; lossy: {}", String::from_utf8_lossy(e.as_bytes())),
}
}
There are several equivalent ways to create an owned String
from a literal.
let a = String::from("one");
let b = "two".to_string();
let c = "three".to_owned();
let d: String = "four".into();
All four allocate a new String and copy the text. Which one
to use is a matter of style.
let s1: &str = &a;
let s2 = b.as_str();
println!("{}", shout(&c));
Going the other way is free. A &String automatically
turns into a &str through deref coercion, so
functions should take &str parameters.
let from_chars: String = chars.iter().collect(); let back: Vec<char> = from_chars.chars().collect();
Characters are collected into a string and split back with
chars.
let bytes = from_chars.clone().into_bytes(); let text = String::from_utf8(bytes).unwrap();
The into_bytes method gives up the string's buffer as a
Vec<u8> without copying. String::from_utf8
takes it back, but validates it first: invalid UTF-8 is rejected. The
from_utf8_lossy function replaces invalid bytes with the
U+FFFD replacement character instead.
$ cargo run -q one two three four ONE TWO THREE x rust ['r', 'u', 's', 't'] [114, 117, 115, 116] rust error: invalid utf-8 sequence of 1 bytes from index 2; lossy: fo�
Parsing strings
The parse method converts a string into any type that
implements FromStr. It returns a Result.
fn main() {
let n: i32 = "42".parse().unwrap();
let f = "3.75".parse::<f64>().unwrap();
let b: bool = "true".parse().unwrap();
let c: char = "z".parse().unwrap();
println!("{n} {f} {b} {c}");
let inputs = ["17", " 17", "17.0", "-17", "", "99999999999", "1_000"];
for s in inputs {
match s.parse::<i32>() {
Ok(v) => println!("{s:?} -> {v}"),
Err(e) => println!("{s:?} -> error: {e}"),
}
}
println!("{:?}", " 17 ".trim().parse::<u8>());
println!("{:?}", "-1".parse::<u8>());
println!("{:?}", "1e3".parse::<f64>());
println!("{:?}", "nan".parse::<f64>());
println!("{:?}", i64::from_str_radix("ff", 16));
}
The target type is given by an annotation or by the turbofish syntax
parse::<f64>().
let inputs = ["17", " 17", "17.0", "-17", "", "99999999999", "1_000"];
Integer parsing is strict. Leading spaces, a decimal point, and the underscores allowed in Rust literals are all invalid digits. Too large numbers and empty strings get their own error messages.
println!("{:?}", " 17 ".trim().parse::<u8>());
println!("{:?}", "-1".parse::<u8>());
Input read from a user or a file usually has whitespace, so we trim it first. A minus sign is an invalid digit for unsigned types.
println!("{:?}", "1e3".parse::<f64>());
println!("{:?}", "nan".parse::<f64>());
println!("{:?}", i64::from_str_radix("ff", 16));
Float parsing accepts scientific notation and the special values
nan and inf. For other number bases, the integer
types have from_str_radix.
$ cargo run -q
42 3.75 true z
"17" -> 17
" 17" -> error: invalid digit found in string
"17.0" -> error: invalid digit found in string
"-17" -> -17
"" -> error: cannot parse integer from empty string
"99999999999" -> error: number too large to fit in target type
"1_000" -> error: invalid digit found in string
Ok(17)
Err(ParseIntError { kind: InvalidDigit })
Ok(1000.0)
Ok(NaN)
Ok(255)
Converting values to strings
Every type that implements Display has a
to_string method.
fn main() {
let n = 1234;
let f = 0.1 + 0.2;
let ok = false;
let c = 'ž';
let parts = [n.to_string(), f.to_string(), ok.to_string(), c.to_string()];
println!("{parts:?}");
let joined = [1, 2, 3].map(|x| x.to_string()).join("-");
println!("{joined}");
let padded = format!("{n:06}");
println!("{padded}");
}
The to_string method produces the same text as
{} in a format string.
let joined = [1, 2, 3].map(|x| x.to_string()).join("-");
To join numbers, we first convert them to strings; join
works only on string slices.
let padded = format!("{n:06}");
When the text needs a width, padding, or precision, we use
format! instead. Formatting is covered in detail in
Rust string formatting.
$ cargo run -q ["1234", "0.30000000000000004", "false", "ž"] 1-2-3 001234
Implementing From
We implement From to define conversions for our own types.
#[derive(Debug)]
struct Rgb {
r: u8,
g: u8,
b: u8,
}
impl From<u32> for Rgb {
fn from(hex: u32) -> Self {
Rgb {
r: (hex >> 16) as u8,
g: (hex >> 8) as u8,
b: hex as u8,
}
}
}
impl From<Rgb> for u32 {
fn from(c: Rgb) -> Self {
(u32::from(c.r) << 16) | (u32::from(c.g) << 8) | u32::from(c.b)
}
}
impl From<(u8, u8, u8)> for Rgb {
fn from((r, g, b): (u8, u8, u8)) -> Self {
Rgb { r, g, b }
}
}
fn main() {
let orange = Rgb::from(0xff8800);
println!("{orange:?}");
let teal: Rgb = (0, 128, 128).into();
println!("{teal:?}");
let hex: u32 = teal.into();
println!("{hex:#08x}");
}
An Rgb color can be created from a hexadecimal number or a
tuple, and converted back to a number.
impl From<u32> for Rgb {
fn from(hex: u32) -> Self {
Rgb {
r: (hex >> 16) as u8,
g: (hex >> 8) as u8,
b: hex as u8,
}
}
}
Here the truncating as cast is exactly what we want: after
shifting, it keeps the lowest byte.
let teal: Rgb = (0, 128, 128).into(); let hex: u32 = teal.into();
We implemented only From; the matching into
methods come from a blanket implementation in the standard library. Always
implement From, not Into.
$ cargo run -q
Rgb { r: 255, g: 136, b: 0 }
Rgb { r: 0, g: 128, b: 128 }
0x008080
Implementing TryFrom
A type with a restricted range of values implements TryFrom
to validate its input.
use std::fmt;
#[derive(Debug)]
struct Percent(u8);
#[derive(Debug)]
struct OutOfRange(i64);
impl fmt::Display for OutOfRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} is not between 0 and 100", self.0)
}
}
impl TryFrom<i64> for Percent {
type Error = OutOfRange;
fn try_from(v: i64) -> Result<Self, Self::Error> {
if (0..=100).contains(&v) {
Ok(Percent(v as u8))
} else {
Err(OutOfRange(v))
}
}
}
fn main() {
for v in [75, 100, 120, -3] {
match Percent::try_from(v) {
Ok(p) => println!("ok: {p:?}, {}%", p.0),
Err(e) => println!("error: {e}"),
}
}
let p: Result<Percent, _> = 50_i64.try_into();
println!("{p:?}");
}
A Percent can only be created from a number between 0 and
100.
impl TryFrom<i64> for Percent {
type Error = OutOfRange;
The associated type Error says what the conversion returns on
failure. Our error type keeps the rejected value and implements
Display for a readable message.
let p: Result<Percent, _> = 50_i64.try_into();
As with From and Into, implementing
TryFrom provides try_into.
$ cargo run -q ok: Percent(75), 75% ok: Percent(100), 100% error: 120 is not between 0 and 100 error: -3 is not between 0 and 100 Ok(Percent(50))
Implementing FromStr
Implementing FromStr makes our type work with
parse.
use std::str::FromStr;
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl FromStr for Point {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (x, y) = s
.trim()
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.and_then(|s| s.split_once(','))
.ok_or_else(|| format!("expected (x, y), got {s:?}"))?;
let x = x.trim().parse().map_err(|e| format!("bad x {x:?}: {e}"))?;
let y = y.trim().parse().map_err(|e| format!("bad y {y:?}: {e}"))?;
Ok(Point { x, y })
}
}
fn main() {
for s in ["(3, 4)", "(-10,25)", "3, 4", "(3, four)"] {
match s.parse::<Point>() {
Ok(p) => println!("{s:<10} -> {p:?}, x + y = {}", p.x + p.y),
Err(e) => println!("{s:<10} -> error: {e}"),
}
}
let points: Result<Vec<Point>, _> = "(1,1) (2,4) (3,9)"
.split(' ')
.map(str::parse::<Point>)
.collect();
println!("{points:?}");
}
We parse points written as (x, y).
let (x, y) = s
.trim()
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.and_then(|s| s.split_once(','))
.ok_or_else(|| format!("expected (x, y), got {s:?}"))?;
Each step returns an Option. If any of them fails, the chain
yields None, which ok_or_else turns into an
error message.
let x = x.trim().parse().map_err(|e| format!("bad x {x:?}: {e}"))?;
The coordinates are parsed as i32, inferred from the struct
fields. The integer error is converted into our String error
type with map_err.
let points: Result<Vec<Point>, _> = "(1,1) (2,4) (3,9)"
.split(' ')
.map(str::parse::<Point>)
.collect();
Collecting an iterator of Results into a
Result<Vec<_>, _> gives either all points or the
first error.
$ cargo run -q
(3, 4) -> Point { x: 3, y: 4 }, x + y = 7
(-10,25) -> Point { x: -10, y: 25 }, x + y = 15
3, 4 -> error: expected (x, y), got "3, 4"
(3, four) -> error: bad y " four": invalid digit found in string
Ok([Point { x: 1, y: 1 }, Point { x: 2, y: 4 }, Point { x: 3, y: 9 }])
Into and AsRef in parameters
Generic parameters with Into and AsRef make
functions accept several argument types.
struct Person {
name: String,
}
impl Person {
fn new(name: impl Into<String>) -> Self {
Person { name: name.into() }
}
}
fn print_all<T: AsRef<str>>(items: &[T]) {
let v: Vec<&str> = items.iter().map(|s| s.as_ref()).collect();
println!("{}", v.join(", "));
}
fn main() {
let a = Person::new("Anna");
let owned = String::from("Boris");
let b = Person::new(owned);
let c = Person::new('C');
println!("{} {} {}", a.name, b.name, c.name);
print_all(&["sky", "sea"]);
print_all(&[String::from("red"), String::from("blue")]);
}
The constructor accepts anything that can be turned into a
String.
fn new(name: impl Into<String>) -> Self {
Person { name: name.into() }
}
A string literal is copied into a new String, an owned
String is moved in without copying, and even a
char works. The caller does not have to write
to_string.
fn print_all<T: AsRef<str>>(items: &[T]) {
The AsRef<str> bound means that we can borrow a
&str from each item. The function accepts slices of
&str as well as slices of String. The
standard library uses AsRef<Path> in the same way for
file functions.
$ cargo run -q Anna Boris C sky, sea red, blue
Converting collections
Collections convert into each other with From,
TryFrom, and collect.
use std::collections::{BTreeMap, BTreeSet, VecDeque};
fn main() {
let arr = [3, 1, 2, 3, 1];
let v = Vec::from(arr);
let v2: Vec<i32> = arr.into();
println!("{v:?} {}", v == v2);
let set = BTreeSet::from(arr);
println!("{set:?}");
let back: [i32; 5] = v.clone().try_into().unwrap();
println!("{back:?}");
let short = vec![1, 2];
let r: Result<[i32; 3], Vec<i32>> = short.try_into();
println!("{r:?}");
let deque = VecDeque::from(v);
println!("{deque:?}");
let map = BTreeMap::from([("b", 2), ("a", 1)]);
let pairs: Vec<(&str, i32)> = map.into_iter().collect();
println!("{pairs:?}");
let boxed: Box<[i32]> = vec![7, 8, 9].into_boxed_slice();
println!("{boxed:?} len {}", boxed.len());
}
Arrays convert into vectors, sets, deques, and maps.
let set = BTreeSet::from(arr);
Converting to a set removes duplicates. A BTreeSet also sorts
its elements.
let back: [i32; 5] = v.clone().try_into().unwrap(); let r: Result<[i32; 3], Vec<i32>> = short.try_into();
A vector converts into an array only if the lengths match, so this is a
TryFrom conversion. On failure we get the original vector
back in the Err variant.
let pairs: Vec<(&str, i32)> = map.into_iter().collect();
Iterating over a map yields key-value tuples, which can be collected into a vector. The collect article shows more target collections.
$ cargo run -q
[3, 1, 2, 3, 1] true
{1, 2, 3}
[3, 1, 2, 3, 1]
Err([1, 2])
[3, 1, 2, 3, 1]
[("a", 1), ("b", 2)]
[7, 8, 9] len 3
Characters and digits
Characters convert to and from code points and digits.
fn main() {
let c = 'é';
println!("{c} = U+{:04X} = {}", c as u32, u32::from(c));
let b: u8 = 97;
println!("{} {}", b as char, char::from(b));
println!("{:?} {:?}", char::from_u32(0x263A), char::from_u32(0xD800));
println!("{:?} {:?} {:?}", '7'.to_digit(10), 'f'.to_digit(16), 'x'.to_digit(16));
println!("{:?} {:?}", char::from_digit(7, 10), char::from_digit(11, 16));
let digits: Vec<u32> = "2026".chars().filter_map(|c| c.to_digit(10)).collect();
println!("{digits:?}");
let shifted: String = "HAL".chars().map(|c| (c as u8 + 1) as char).collect();
println!("{shifted}");
}
A char is a Unicode scalar value, so the conversions to
u32 always work.
println!("{} {}", b as char, char::from(b));
println!("{:?} {:?}", char::from_u32(0x263A), char::from_u32(0xD800));
Only u8 converts to char with as or
From, because every byte value is a valid code point. A
u32 may not be one (0xD800 is a surrogate), so
char::from_u32 returns an Option.
let digits: Vec<u32> = "2026".chars().filter_map(|c| c.to_digit(10)).collect();
The to_digit method converts a digit character to its numeric
value in the given base, and char::from_digit does the
opposite.
let shifted: String = "HAL".chars().map(|c| (c as u8 + 1) as char).collect();
Arithmetic on characters goes through a numeric type. The cast to
u8 is correct here only because the letters are ASCII.
$ cargo run -q
é = U+00E9 = 233
a a
Some('☺') None
Some(7) Some(15) None
Some('7') Some('b')
[2, 0, 2, 6]
IBM
Numbers and bytes
Numbers convert to and from their byte representation, which is needed for binary files and network protocols.
fn main() {
let n: u32 = 0x12345678;
println!("{:02x?}", n.to_be_bytes());
println!("{:02x?}", n.to_le_bytes());
let data = [0x78, 0x56, 0x34, 0x12, 0xff, 0xff];
let first = u32::from_le_bytes(data[..4].try_into().unwrap());
let second = i16::from_le_bytes([data[4], data[5]]);
println!("{first:#x} {second}");
let x = 3.5_f32;
let bits = x.to_bits();
println!("{x} = {bits:#010x} = {:?}", x.to_be_bytes());
println!("{}", f32::from_bits(bits));
}
The to_be_bytes and to_le_bytes methods return
an array of bytes in big-endian or little-endian order.
let first = u32::from_le_bytes(data[..4].try_into().unwrap());
The from_le_bytes function needs an array of exactly four
bytes. A slice is converted into such an array with
try_into, which fails if the slice has a different length.
let bits = x.to_bits();
For floats, to_bits returns the raw IEEE 754 bits as an
integer and from_bits converts them back. This is different
from x as u32, which converts the numeric value.
$ cargo run -q [12, 34, 56, 78] [78, 56, 34, 12] 0x12345678 -1 3.5 = 0x40600000 = [64, 96, 0, 0] 3.5
Source
std::convert module documentation
The Rust Reference: Type cast expressions
In this article we have converted values between types in Rust. Number to
string conversions are covered in more detail in
Rust int to string, and error conversions
with From and ? in
Rust error handling.
Author
List all Rust tutorials.