When the string isn't a number yet
You're building a CLI tool that asks for a port number. The user types 8080. Great. The user types abc. In Python, you'd get a ValueError. In JavaScript, you might get NaN or a silent failure that breaks math later. In Rust, the compiler forces you to decide what happens before the code even runs. You can't just call a conversion function and hope for the best. Rust makes the failure explicit.
This is the core of parsing: turning untrusted text into a typed value while handling the inevitable mistakes. Rust's approach is simple but strict. The parser returns a Result. You must handle the error. The compiler won't let you ignore it.
The strict validator
Think of parse like a customs officer at a border. You hand over a string and declare the target type. The officer checks every character against the rules for that type. If you declare "Integer" and the string is 3.14, the officer rejects it. If you declare "Float", it passes. The officer never guesses what you meant. If the string contains a letter, a comma, or a number that's too large, the officer hands you back an error ticket.
This design prevents silent data corruption. You never get a number when you asked for a number but the input was garbage. You get an error you can log, retry, or report to the user.
Minimal example
The standard way to parse is the parse method on &str. It's generic, so you must specify the target type.
fn main() {
let input = "42";
// parse is generic; the compiler needs to know the target type.
// Turbofish syntax specifies u32 explicitly.
let result = input.parse::<u32>();
// Result forces you to handle both success and failure.
match result {
Ok(num) => println!("Got number: {}", num),
Err(e) => println!("Parse failed: {}", e),
}
}
The match arm handles the Err case. If the input is abc, the program prints the error instead of crashing. The error message tells you exactly what went wrong, like "invalid digit found in string".
The compiler forces you to handle the error. You can't ignore it.
Under the hood: FromStr and Result
parse is syntax sugar for the FromStr trait. Every numeric type implements FromStr. When you write s.parse::<i32>(), Rust looks up the FromStr implementation for i32. That implementation scans the string, validates characters, checks for overflow, and returns a Result<i32, ParseIntError>.
The error type depends on the target. Parsing an integer returns ParseIntError. Parsing a float returns ParseFloatError. This is why the compiler needs the type annotation: the error type is part of the return signature. If you try to parse a type that doesn't implement FromStr, the compiler rejects you with E0277 (trait bound not satisfied).
use std::str::FromStr;
// Custom type that can be parsed.
#[derive(Debug)]
struct Port(u16);
// Implement FromStr to enable parse() on Port.
impl FromStr for Port {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Delegate to u16 parsing, then wrap.
let num: u16 = s.parse().map_err(|_| "invalid port")?;
// Validate business logic constraints.
if num == 0 {
return Err("port cannot be zero");
}
Ok(Port(num))
}
}
fn main() {
// parse works on custom types too.
let p: Port = "8080".parse().unwrap();
println!("{:?}", p);
}
Implementing FromStr lets you use parse on your own types. This keeps the API consistent across the standard library and your code.
The error type matches the target type. You get a ParseIntError for integers, never a float error.
Real-world input needs trimming
User input often contains accidental whitespace. A config file might have 100 . A text box might have a trailing newline. parse treats whitespace as invalid. You must clean the string first.
The convention is to call trim() before parsing. trim removes leading and trailing whitespace, including spaces, tabs, and newlines.
fn main() {
let user_input = " 123 ";
// Trim removes leading/trailing whitespace.
// parse returns Result, so ? propagates the error immediately.
let number: i32 = user_input.trim().parse().unwrap();
println!("Number: {}", number);
}
In a function that returns Result, use the ? operator to propagate errors cleanly.
use std::num::ParseIntError;
fn get_port(config_line: &str) -> Result<u16, ParseIntError> {
// Trim handles accidental spaces around the number.
// Type inference works because the return type specifies u16.
let port: u16 = config_line.trim().parse()?;
Ok(port)
}
The ? operator checks the Result. If it's Ok, it unwraps the value. If it's Err, it returns the error from the function. This keeps error handling concise.
Always trim user input before parsing. Whitespace is the silent killer of parsers.
Locale and formatting rules
Rust's parsing is locale-agnostic. It always expects a dot for decimal separators. If your input uses a comma like 3,14, parsing fails. This is intentional. Systems code needs consistent behavior regardless of the user's region.
fn main() {
let german_float = "3,14";
// This fails because Rust expects a dot.
let result = german_float.parse::<f64>();
match result {
Ok(f) => println!("{}", f),
Err(_) => println!("Comma not supported. Use dot."),
}
}
If you need to support commas, replace them before parsing.
fn main() {
let input = "3,14";
// Replace comma with dot for parsing.
let normalized = input.replace(',', ".");
let f: f64 = normalized.parse().unwrap();
println!("{}", f);
}
This gives you full control over formatting. You decide when to accept commas, and you can log or reject them if needed.
Rust parsing is locale-agnostic. Expect a dot. Replace commas yourself if your data uses them.
Floats, scientific notation, and limits
Float parsing handles more than just 3.14. It supports scientific notation, infinity, and NaN.
fn main() {
// Scientific notation works out of the box.
let sci: f64 = "1e5".parse().unwrap();
println!("1e5 = {}", sci); // 100000.0
// Infinity and NaN are valid floats.
let inf: f64 = "inf".parse().unwrap();
let nan: f64 = "nan".parse().unwrap();
println!("Inf: {}, NaN: {}", inf, nan);
}
Scientific notation is common in config files and data formats. Rust handles it automatically. You don't need a special parser.
Integer parsing has strict range checks. If the number is too big for the type, you get an error.
fn main() {
let big = "9999999999";
// u32 max is ~4 billion. This overflows.
let result = big.parse::<u32>();
match result {
Ok(n) => println!("{}", n),
Err(e) => println!("Overflow: {}", e),
}
}
The error message indicates the number is out of range. The parser never wraps or truncates. It returns an error.
Scientific notation works automatically. 1e5 parses to 100000.0 without extra work.
Pitfalls and compiler errors
Parsing fails at compile time if the type is ambiguous. If you write let x = "42".parse();, the compiler rejects you with E0283 (type annotations needed). You must specify the type via turbofish or annotation.
fn main() {
// This fails: compiler doesn't know if you want i32, u64, f64, etc.
// let x = "42".parse();
// Fix: specify type.
let x: i32 = "42".parse().unwrap();
}
Another common mistake is calling unwrap() on untrusted input. unwrap() panics on Err. In production code, this crashes your program.
fn main() {
let input = "abc";
// This panics at runtime.
// let n: u32 = input.parse().unwrap();
// Safe: handle the error.
match input.parse::<u32>() {
Ok(n) => println!("{}", n),
Err(_) => println!("Invalid input"),
}
}
The panic comes from unwrap, not from parse. parse returns an error safely. You choose to panic by ignoring the error.
The parser returns an error on overflow. The panic comes from calling unwrap on that error.
Decision: choosing the right parser
Use parse::<T>() when you need to convert a decimal string to a standard numeric type like i32, u64, or f64.
Use i32::from_str_radix(s, 16) when you are parsing hexadecimal, binary, or other bases that aren't decimal.
Use trim().parse() when your input comes from user input or files that might contain accidental whitespace.
Use parse().unwrap() only in tests or quick scripts where invalid input is impossible by construction.
Use parse() with match or ? in production code to handle invalid input gracefully.
Use replace(',', '.').parse() when your data uses commas as decimal separators and you need to normalize it for Rust's parser.
Stick to parse for decimal. Reach for from_str_radix when the base changes.