Question
I want to convert between numeric types in Rust safely and idiomatically.
For example, how should I convert a usize to a u32?
A direct cast like this works:
let x: usize = 4_294_967_295;
let y = x as u32;
But a cast can silently truncate or overflow:
let x: usize = 4_294_967_296;
let y = x as u32; // becomes 0
Older Rust discussions mention traits such as ToPrimitive and FromPrimitive, which return values like Option<u32>, but those APIs were unstable at the time.
So what is the modern, idiomatic, and safe way to convert between numeric types and pointer-sized types in Rust?
I am especially interested in cases involving usize and isize, since their size depends on the platform. For example, if I store tree indices in a Vec<u32> and later use them for indexing:
let t = vec![0u32, 0u32, 1u32];
let grandparent = t[t[2] as usize];
how should this kind of conversion be handled safely, and what happens on platforms where usize is smaller than u32?
Short Answer
By the end of this page, you will understand when to use as, From, Into, TryFrom, and TryInto for numeric conversions in Rust. You will also learn why pointer-sized integers like usize need special care, how to avoid silent truncation, and how to write conversions that are safe across platforms.
Concept
Rust has multiple ways to convert numeric types, and each one has a different safety story.
The core idea is this:
- Use
From/Intowhen the conversion is guaranteed to succeed. - Use
TryFrom/TryIntowhen the conversion might fail. - Use
aswhen you intentionally want Rust's casting behavior, including truncation or reinterpretation rules.
This matters because not all numeric conversions are equally safe.
For example:
u8tou32is always safe because everyu8value fits inu32.u32tou8is not always safe because manyu32values are too large foru8.u32tousizedepends on the platform, becauseusizeis 32-bit on some systems and 64-bit on others.
In modern Rust, the idiomatic approach is to make that risk explicit.
Why as can be dangerous
The as keyword performs a cast. For numeric types, that often means:
- truncating large integers
- changing signedness
- dropping high bits
- converting floating-point values with special rules
Example:
let x: u32 = 300;
let y = x as u8;
assert_eq!(y, 44);
This does not fail. It wraps/truncates the value to fit the destination type.
That behavior is sometimes useful, but it is not a good default when data loss would be a bug.
Safe conversions with TryFrom
If a conversion may fail, use TryFrom:
use std::convert::TryFrom;
let x: usize = 100;
let y = u32::try_from(x);
The result is:
Result<u32, TryFromIntError>
That means Rust forces you to handle the possibility that x does not fit into u32.
Platform-dependent integer sizes
usize and isize are special:
- they are designed for indexing and pointer-sized values
- their width depends on the target architecture
- they should not be assumed to match
u32oru64
Typical sizes:
- 32-bit platform:
usizeis 32 bits - 64-bit platform:
usizeis 64 bits
So:
u32 -> usizeis safe on many common platforms, but not universally guaranteed by the type systemusize -> u32may fail on 64-bit systems if the value is too large
That is why explicit checked conversion is the idiomatic choice when portability matters.
Pointer conversions
Pointer conversions are a separate concern from ordinary numeric conversions. Rust allows some pointer casting, but pointer-to-integer and integer-to-pointer conversions are low-level operations and should be avoided unless necessary.
For ordinary application code, prefer:
- indices as
usize - lengths as
usize - explicit checked conversion for stored numeric IDs
That keeps your code aligned with Rust's collection APIs and reduces bugs.
Mental Model
Think of each integer type as a container with a fixed capacity.
- A
u8is a tiny box. - A
u32is a bigger box. - A
usizeis a box whose size depends on the machine.
Now imagine pouring water from one box into another:
- If you pour from a small box into a bigger box, everything fits.
- If you pour from a big box into a smaller box, some water may spill.
Fromis used when Rust knows nothing can spill.TryFromis used when Rust says this might spill, so check first.asis like saying pour it anyway.
For usize, the box changes size depending on the computer. That is why conversions involving usize should be treated carefully in portable code.
Syntax and Examples
Core conversion tools in Rust
1. From for guaranteed-safe conversions
let x: u8 = 42;
let y: u32 = u32::from(x);
Use this when the destination type can always represent the source value.
2. TryFrom for checked conversions
use std::convert::TryFrom;
let x: usize = 42;
let y: Result<u32, _> = u32::try_from(x);
This is the idiomatic choice when the conversion might fail.
3. Into and TryInto
These are often used when type inference reads better from left to right.
use std::convert::TryInto;
let x: usize = 42;
let y: Result<u32, _> = x.try_into();
4. as for explicit casts
let x: u32 = 300;
let y = x as u8;
println!("{}", y); // 44
Use as only when truncation or cast semantics are intentional.
Beginner-friendly example
use std::convert::TryFrom;
fn main() {
let index_from_file: u32 = 12;
let index = usize::try_from(index_from_file)
.expect("u32 index must fit into usize on this platform");
let values = vec![10, 20, 30, 40, 50];
println!("{}", values[index]);
}
Explanation
- The index is stored as
u32. Vecindexing usesusize.usize::try_from(index_from_file)checks whether the value fits.expect(...)makes the failure message clear if it does not.
If you know the conversion cannot fail in your program's context, expect is often a reasonable choice. Otherwise, return the error and handle it properly.
Step by Step Execution
Consider this example:
use std::convert::TryFrom;
fn main() {
let raw: usize = 500;
let converted = u8::try_from(raw);
match converted {
Ok(value) => println!("Converted: {}", value),
Err(_) => println!("Value does not fit in u8"),
}
}
Step by step
rawis created with the value500.- Rust tries to convert
500fromusizeintou8. - A
u8can only store values from0to255. - Since
500is too large, the conversion fails. u8::try_from(raw)returnsErr(...).- The
matchexpression handles that error and prints:
Value does not fit in u8
Compare with as
fn main() {
let raw: usize = 500;
let converted = raw as u8;
println!("{}", converted);
}
This prints:
244
Why? Because as truncates the value to fit into 8 bits. That may be surprising if you expected a checked conversion.
Real World Use Cases
Indexing collections
Rust collections like Vec, slices, and strings use usize for indexing.
use std::convert::TryFrom;
fn get_item(items: &[String], id: u32) -> Option<&String> {
let index = usize::try_from(id).ok()?;
items.get(index)
}
This is common when IDs come from files, APIs, or databases.
Parsing external data
You may read a large integer from JSON, a config file, or user input and need to store it in a smaller type.
use std::convert::TryFrom;
fn parse_port(value: u32) -> Result<u16, String> {
u16::try_from(value).map_err(|_| "port out of range".to_string())
}
Interfacing with binary formats
Binary formats often specify exact integer widths like u16, u32, or u64. Your program may still need usize for memory operations.
use std::convert::TryFrom;
fn allocate(len: u32) -> Result<Vec<u8>, String> {
let size = usize::try_from(len).map_err(|_| "length too large for this platform".to_string())?;
Ok(vec![0; size])
}
Database or network identifiers
An API may send IDs as u64, while your local logic uses usize or u32.
Checked conversion prevents invalid assumptions from leaking into the rest of the code.
Real Codebase Usage
In real Rust codebases, developers usually follow a few practical patterns.
Prefer the right type from the beginning
If a value is used for indexing, store it as usize when possible.
struct Node {
parent: usize,
}
This avoids repeated conversions.
Convert at boundaries
If data enters your system as u32, convert it once when parsing or validating.
use std::convert::TryFrom;
fn parse_index(raw: u32) -> Result<usize, String> {
usize::try_from(raw).map_err(|_| "index does not fit into usize".to_string())
}
This keeps the rest of the program simpler.
Use guard clauses
use std::convert::TryFrom;
fn get_parent(tree: &[u32], node: usize) -> Option<u32> {
let parent_index_u32 = *tree.get(node)?;
let parent_index = usize::try_from(parent_index_u32).ok()?;
tree.get(parent_index).copied()
}
This pattern avoids panics and handles invalid data safely.
Use expect only when the invariant is guaranteed
let index = usize::try_from(id).expect("validated ID should fit in usize");
This is common after earlier validation or when a platform assumption is intentional.
Avoid as in validation-sensitive code
In production code, unchecked as casts are often discouraged unless the truncation is deliberate and documented.
Common Mistakes
1. Using as when you actually need validation
Broken example:
let big: u32 = 1000;
let small = big as u8;
This compiles, but small becomes 232, not an error.
Better
use std::convert::TryFrom;
let big: u32 = 1000;
let small = u8::try_from(big);
assert!(small.is_err());
2. Assuming usize is always u64
Broken assumption:
let x: u32 = 10;
let y: usize = x as usize;
This may work on your machine, but it bakes in an unchecked assumption about target architecture.
Better
use std::convert::TryFrom;
let x: u32 = 10;
let y = usize::try_from(x).expect("u32 must fit into usize");
3. Indexing without bounds checks
Broken example:
let t = vec![0u32, 0u32, 1u32];
let grandparent = t[t[2] as usize];
This can panic if the converted index is out of bounds.
Better
use std::convert::TryFrom;
let t = vec![0u32, 0u32, 1u32];
let grandparent = usize::try_from(t[2])
.ok()
.and_then(|i| t.get(i))
.copied();
4. Converting too late and too often
If you repeatedly convert the same value, your code becomes noisy and easier to get wrong.
Better approach
Convert once at the boundary and use the correct type internally.
5. Confusing numeric conversion with pointer conversion
Pointer casts are low-level and should not be treated like ordinary integer conversions. Use them only when necessary and with care.
Comparisons
| Conversion tool | Fails at runtime? | Checked for fit? | Typical use |
|---|---|---|---|
as | No | No | Intentional cast, truncation, low-level conversions |
From | No | Yes, by type design | Widening or guaranteed-safe conversions |
Into | No | Yes, via From | Same as From, often nicer with inference |
TryFrom | Yes, returns Err | Yes | Narrowing or platform-dependent conversions |
TryInto | Yes, returns Err | Yes | Same as TryFrom, often nicer in method chains |
From vs TryFrom
| Situation | Use |
|---|---|
| Every source value fits in destination | From |
| Some source values may not fit | TryFrom |
TryFrom vs as
| Need | Best choice |
|---|---|
| Preserve correctness and detect invalid values | TryFrom |
| Intentionally truncate bits | as |
Write portable code involving usize | TryFrom |
For your tree example
| Approach | Result |
|---|---|
Store indices as u32 and cast with as usize | Works, but unchecked |
Store indices as u32 and use usize::try_from(...) | Safe and explicit |
Store indices as usize from the start | Simplest if indices are only used locally |
Cheat Sheet
// Guaranteed-safe conversion
let x: u8 = 5;
let y: u32 = u32::from(x);
// Checked conversion
use std::convert::TryFrom;
let n: usize = 100;
let m: u32 = u32::try_from(n)?;
// Equivalent style
use std::convert::TryInto;
let m: u32 = n.try_into()?;
// Unchecked cast
let a: u32 = 300;
let b: u8 = a as u8; // 44
Rules of thumb
- Use
Fromfor always-safe conversions. - Use
TryFromfor possibly failing conversions. - Use
asonly when cast behavior is intentional. - Be careful with
usizeandisizebecause their size depends on the platform. - For collection indices, prefer
usizeinternally. - Convert external numeric data at the boundary of your program.
Common safe patterns
let i = usize::try_from(id).ok()?;
let value = items.get(i)?;
let size = usize::try_from(len)
.map_err(|_| "length too large")?;
Edge case reminder
u32 -> usizeis not universally guaranteed on every possible platform.usize -> u32may fail on 64-bit systems for large values.asdoes not protect you from truncation.
FAQ
How do I safely convert usize to u32 in Rust?
Use u32::try_from(value). It returns a Result, so you must handle the case where the value is too large.
Should I use as for numeric conversions in Rust?
Only when truncation or cast semantics are intentional. For correctness-sensitive code, prefer From or TryFrom.
What is the difference between From and TryFrom in Rust?
From is for conversions that always succeed. TryFrom is for conversions that may fail because the destination type cannot represent all source values.
Why does Rust use usize for indexing?
usize matches the platform's pointer size, which makes it the correct type for indexing memory-backed collections like Vec and slices.
Can converting u32 to usize fail?
Yes, in portable code it can. On common 32-bit and 64-bit platforms it usually works, but Rust does not treat it as universally guaranteed in every context, so checked conversion is better when portability matters.
What should I store in a vector for indices: u32 or usize?
If the values are only used as indices in memory, usize is often the simplest choice. If you need a fixed-width format for storage or networking, store u32 and convert to usize at the boundary.
How do I avoid panics when converting and indexing?
Use checked conversion plus .get():
let result = usize::try_from(raw_index)
.ok()
.and_then(|i| items.get(i));
Are pointer casts the same as numeric conversions?
No. Pointer casts are lower-level operations with different safety concerns. Avoid them unless you are writing systems-level or FFI code.
Mini Project
Description
Build a small Rust program that reads parent indices from a vector and safely finds a node's grandparent. This demonstrates checked numeric conversion from u32 to usize and safe vector access without panics.
Goal
Write a function that returns a node's grandparent from a tree stored as Vec<u32>, using checked conversion and bounds-safe indexing.
Requirements
- Store the tree as a
Vec<u32>where each value represents a parent index. - Write a function that takes the tree and a node index.
- Convert
u32parent indices tousizesafely. - Avoid panics by using safe indexing.
- Return
Option<u32>when the grandparent may not exist.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.