How to choose between i32 and usize
Short answer: use i32 for numbers you do arithmetic with — it is the default integer type, and it can be negative. Use usize for anything that measures memory: indices, lengths, capacities. Rust does not leave this as a style choice — indexing a Vec with an i32 does not compile, and len() hands you a usize whether you wanted one or not.
From a fresh project — cargo new usize-demo, then replace src/main.rs with each block below.
What each one is
Section titled “What each one is”fn main() { let count: i32 = -42; // signed: negatives allowed let index: usize = 3; // unsigned: counting positions println!("i32 is {} bytes wide", std::mem::size_of::<i32>()); println!("usize is {} bytes wide on this machine", std::mem::size_of::<usize>()); println!("{count} {index}");}i32 is 4 bytes wideusize is 8 bytes wide on this machine-42 3i32 is signed and exactly 32 bits on every platform: −2,147,483,648 to 2,147,483,647. Write let x = 5; with no type annotation and this is the type you get — the compiler’s default.
usize is unsigned, and its size matches a pointer: 4 bytes on a 32-bit target, 8 on a 64-bit one. This machine is 64-bit, so the run above prints 8. Its maximum is the largest offset a memory address can express, which is why it — and not i32 — is the type of every length, index, and capacity in the standard library: a collection can never hold more elements than there are addressable positions.
The unsigned part matters too: a usize has no negative values, so let i: usize = -1; is a compile error, not a wrapped-around giant number.
Where usize is forced on you
Section titled “Where usize is forced on you”The most common encounter is indexing. This does not compile:
fn main() { let v = vec![10, 20, 30]; let i: i32 = 1; println!("{}", v[i]);}cargo run -qerror[E0277]: the type `[{integer}]` cannot be indexed by `i32` --> src/main.rs:4:22 | 4 | println!("{}", v[i]); | ^ slice indices are of type `usize` or ranges of `usize` | = help: the trait `SliceIndex<[{integer}]>` is not implemented for `i32`help: `usize` implements trait `SliceIndex<T>` --> /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/slice/index.rs:179:1 |179 | const unsafe impl<T> SliceIndex<[T]> for usize { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SliceIndex<[T]>` | ::: /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/bstr/traits.rs:197:1 |197 | unsafe impl SliceIndex<ByteStr> for usize { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SliceIndex<ByteStr>` = note: required for `Vec<{integer}>` to implement `Index<i32>`
For more information about this error, try `rustc --explain E0277`.error: could not compile `usize-demo` (bin "usize-demo") due to 1 previous errorIndexing goes through the SliceIndex trait, and i32 does not implement it — only usize (and ranges of usize) do, because indexing is a memory-address calculation. The fix is to make the variable the right type in the first place when you can:
fn main() { let v = vec![10, 20, 30]; let i: usize = 1; println!("{}", v[i]);}20When the index comes from a loop, you rarely write the type at all — enumerate already produces a usize:
fn main() { let scores = vec![90, 85, 77]; for (i, s) in scores.iter().enumerate() { println!("{i}: {s}"); }}0: 901: 852: 77Mixing them in one expression
Section titled “Mixing them in one expression”Even without indexing, the two types refuse to meet in one expression. len() returns usize; compare an i32 against it and the compiler stops you:
fn main() { let v = vec![10, 20, 30]; let n: i32 = 5; let ok = n < v.len(); println!("{ok}");}cargo run -qerror[E0308]: mismatched types --> src/main.rs:4:18 |4 | let ok = n < v.len(); | - ^^^^^^^ expected `i32`, found `usize` | | | expected because this is `i32` |help: you can convert a `usize` to an `i32` and panic if the converted value doesn't fit |4 | let ok = n < v.len().try_into().unwrap(); | ++++++++++++++++++++
For more information about this error, try `rustc --explain E0308`.error: could not compile `usize-demo` (bin "usize-demo") due to 1 previous errorThe same wall stands between the types for +, - and every other operator — there is no implicit conversion anywhere in Rust. The compiler’s own suggestion (try_into().unwrap()) works, but it panics on values that do not fit, which trades a compile-time error for a run-time one. The better fix is the one from the indexing error: pick one type for the job. If n counts positions too, declare it usize and the expression just works.
The cast that lies
Section titled “The cast that lies”Sometimes you genuinely have an i32 — a value parsed from input, a number from an API — and need a usize. The tempting fix is as usize:
fn main() { let n: i32 = -1; let u = n as usize; println!("{u}");}18446744073709551615A negative number becomes a 19-digit one. as from a signed type to an unsigned one wraps around: −1 becomes the largest value usize can hold. The compiler allows it, and nothing at run time warns you — the bug surfaces much later, when that “index” walks off the end of a slice or quietly compares as huge.
TryFrom makes the failure a value you have to deal with instead:
fn main() { let n: i32 = -1; match usize::try_from(n) { Ok(u) => println!("converted: {u}"), Err(e) => println!("no: {e}"), }}no: number too small to fit in target typeusize::try_from(n) returns a Result: Ok for every value that fits, Err for the negatives (and, going the other way, for values too big for the target). It is the same shape as the parse result in How to convert a String to a number — conversion that can fail returns the failure as a value.
Which one, when
Section titled “Which one, when”- Counting positions in memory — indices,
len(), capacities, loop counters over a collection —usize. The standard library already chose it; matching it means no casts at all. - Numbers with their own meaning — temperatures, scores, user input, anything that can legitimately be negative —
i32. It is the default, it fits almost every real-world range, and it will not silently become 18 quintillion. - Between them, convert with
try_from, notas.asis for the rare case where wrapping is exactly what you want; for everything else it turns a type error into a wrong number.
The overflow question — what happens when a number grows past its type’s limit — is How to stop an integer from overflowing’s subject; the same limits apply to both types here, they just sit in different places.
- Whether to use
f32orf64is the same question one row down, for floats — and there the default is different. - Working with numbers — the rest of the chapter.