How to Convert a String to an Integer in Rust

Convert a string to an integer in Rust using the parse method with error handling.

The strict bouncer at the door

You are reading a configuration file. The value for max_retries arrives as " 3 ". You need to add one to it, multiply it by ten, or pass it to a loop. In Python, you call int(). In JavaScript, you use Number() or parseInt(). Rust refuses to guess. It hands you a Result and forces you to decide what happens when the string contains letters, exceeds the maximum value, or carries hidden whitespace.

Why Rust refuses to guess

String to integer conversion is not a silent coercion. It is a validation step. Think of it like a toll booth operator checking a ticket. The operator looks at the characters, verifies they form a valid number, checks that the value fits in the designated lane, and then either stamps the ticket and lets you through or hands you a rejection notice. Rust builds this validation into the language because silent failures in data pipelines cause silent corruption in memory. The parse() method lives on &str and String. It always returns a Result<T, ParseIntError>. You get the number on success, or a structured error on failure.

The language deliberately avoids implicit conversions. "42" is a sequence of UTF-8 bytes. 42 is a binary representation in memory. Bridging that gap requires decisions about size, sign, and error handling. Rust makes those decisions explicit. You pick the integer type. You pick the error strategy. The compiler enforces both choices before your code runs.

The minimal working example

/// Converts a string slice to a signed 32-bit integer.
fn basic_parse() {
    let input = "42";
    // Type annotation tells the compiler which integer size to target.
    // Without it, the compiler cannot choose between i8, i32, i64, etc.
    let value: i32 = input.parse().expect("Failed to parse integer");
    
    println!("Parsed value: {}", value);
}

The type annotation : i32 is mandatory. Rust has dozens of integer types. i8, u16, i64, usize. The compiler needs to know which one you want before it can generate the parsing logic. The expect call unwraps the Result. It prints your message and panics if parsing fails.

Community convention favors the colon annotation over the turbofish syntax .parse::<i32>(). The colon keeps the call chain readable and matches how you declare variables elsewhere. Both compile to identical machine code. Pick the one that scans faster in your codebase.

Walking through compilation and runtime

When you compile this, the compiler resolves .parse() to the FromStr trait implementation for i32. It inserts the type hint you provided into the method call. If you omit the hint, the compiler rejects the code with E0282 (type annotations needed). It cannot infer the target size from the right-hand side alone.

At runtime, the function iterates over the UTF-8 bytes. It skips nothing. It validates each character against the allowed set for the target type. It tracks the sign. It accumulates the value using checked arithmetic. If it encounters a letter, a space, or a character outside the ASCII digit range, it stops immediately and returns Err(ParseIntError). If the accumulated value exceeds the maximum or minimum bounds of i32, it returns an overflow error. The process is strict by design. It catches bad data before it reaches your business logic.

The FromStr trait is the engine behind parse(). It is a standard library contract that says "this type knows how to construct itself from a string." You can implement it for your own types. The standard library provides it for all numeric primitives, bool, char, and several standard collections. When you call .parse(), you are invoking that trait.

Real world usage: handling input gracefully

Real input is messy. Users add spaces. CSV files contain trailing commas. Network payloads might carry negative signs in unexpected places. You need a pattern that trims, parses, and handles the error without crashing the entire program.

/// Reads a configuration value and returns a default if parsing fails.
fn read_config_value(raw: &str) -> i32 {
    // Strip leading and trailing whitespace before attempting conversion.
    // parse() rejects spaces, so trimming prevents immediate failure.
    let trimmed = raw.trim();
    
    // Attempt parsing and provide a fallback instead of panicking.
    // This keeps the application running even with malformed config.
    match trimmed.parse::<i32>() {
        Ok(n) => n,
        Err(_) => 0,
    }
}

The trim() call is a community convention for user-facing input. It saves you from writing custom whitespace filters. The match arm handles the Result explicitly. You decide the fallback behavior. This pattern appears in CLI parsers, config loaders, and API handlers.

When you are inside a function that already returns Result, you can drop the match entirely. The ? operator propagates the error upward. It unwraps Ok values and returns early on Err. This keeps your happy path visible and pushes error handling to the boundary of your module.

/// Parses a command line argument and propagates errors to the caller.
fn parse_port_arg(arg: &str) -> Result<u16, std::num::ParseIntError> {
    // The ? operator unwraps the Result or returns early with the error.
    // It requires the function signature to return a compatible Result type.
    let port = arg.trim().parse::<u16>()?;
    
    // Validate domain constraints after successful parsing.
    // Port numbers must be in the valid network range.
    if port == 0 {
        return Err(std::num::ParseIntError::from(std::num::IntErrorKind::InvalidDigit));
    }
    
    Ok(port)
}

The ? operator is syntactic sugar for a match that returns Err(e). It works because ParseIntError implements From for itself. You can chain multiple ? calls. The compiler generates the early return logic for you. Use it to keep parsing pipelines clean.

The error anatomy and common traps

Missing the type annotation triggers E0282. The compiler cannot infer the target integer size. You must write : i32 or use the turbofish syntax. The community prefers the colon annotation for readability.

Whitespace is the most common runtime trap. " 42" fails. "42 " fails. "42\n" fails. The parser expects exact digit sequences. Always chain .trim() before .parse() when the source is untrusted.

Overflow is silent in other languages but explicit here. Parsing "99999999999999999999" into an i32 returns an error. The value exceeds i32::MAX. You must either widen the type to i64 or clamp the input. The error variant ParseIntErrorKind::PosOverflow tells you exactly what happened.

Locale formatting breaks parsing instantly. "1,000" fails. "1.000" fails. Rust does not parse human-readable number formats. You must strip non-digit characters or use a dedicated formatting library before calling parse().

Type mismatches surface at compile time. If you assign a Result<i32, _> to a variable typed as i64, the compiler rejects it with E0308 (mismatched types). The Result wrapper does not auto-coerce. You must unwrap or map the value explicitly.

Choosing the right parsing strategy

Use .parse() with a colon type annotation when you control the input format and need a quick conversion in scripts or tests. Use .trim().parse() when reading from files, CLI arguments, or network payloads where whitespace is unpredictable. Use match or if let on the Result when you need to log the specific error or provide a domain-specific fallback. Use the ? operator inside functions that already return Result to propagate parsing failures upward without boilerplate. Use i64::from_str_radix(str, 16) when you are working with hexadecimal memory addresses or color codes and need explicit base control. Use usize for array indices and isize for pointer arithmetic, but stick to i32 or i64 for domain values to avoid platform-dependent size surprises.

Trust the Result. It is not a nuisance. It is a contract that keeps your data pipeline honest.

Where to go next