The Goblin Needs a Dice Roll
You're building a text adventure. The goblin attacks. You need a damage roll. You reach for a random number function, type rand(), and the compiler tells you that function doesn't exist. You check the standard library docs. Nothing. Rust doesn't ship with a random number generator. The standard library stays focused on deterministic, safe systems code. Randomness requires entropy, and entropy requires platform-specific details that the standard library avoids. You need the rand crate.
Why Rust Doesn't Ship with Randomness
The Rust standard library prioritizes predictability. System libraries must behave the same way every time they run. Randomness breaks that rule. Generating high-quality random numbers also involves security considerations. A bad random number generator can leak secrets or crash under load. The Rust ecosystem solves this by pushing randomness into a community-maintained crate. The rand crate is the de facto standard. It's fast, safe, and battle-tested. It handles the messy details of entropy collection, algorithm selection, and bias correction so you don't have to.
The rand Crate and the Rng Trait
Add rand to your Cargo.toml. The crate exposes a central trait called Rng. This trait defines the interface for all random number generators. Methods like gen_range live on the trait, not on the generator structs. This design lets you swap generators without changing your logic. You can write a function that accepts impl Rng and test it with a seeded generator while running it with a fast, auto-seeded generator in production.
Think of the Rng trait as a universal remote. Different generators are different TV brands. The remote has the same buttons for every brand. You press gen_range, and the remote talks to whatever generator you're holding.
Quick Randomness with thread_rng
For most applications, you want rand::thread_rng(). This function returns a thread-local random number generator that initializes automatically using OS entropy. It caches the generator per thread, so calling it repeatedly is cheap. The first call pays the entropy cost. Subsequent calls reuse the cached instance.
use rand::Rng;
fn main() {
// thread_rng() returns a cached, auto-seeded generator for the current thread.
// It's the standard choice for application code.
let mut rng = rand::thread_rng();
// gen_range takes a Range or RangeInclusive.
// 1..=6 produces values from 1 to 6 inclusive.
let dice_roll = rng.gen_range(1..=6);
println!("You rolled a {}", dice_roll);
}
Convention aside: always use rand::thread_rng() when you just need a number. Don't construct a StdRng manually unless you have a specific reason. The community expects thread_rng for general purpose randomness. It's optimized and safe.
Trust gen_range. It knows more about statistics than you do.
The Hidden Trap: Modulo Bias
Beginners often try to roll dice using the modulo operator. This introduces bias. If you generate a random u32 and take value % 6, the results aren't uniform. The u32 range doesn't divide evenly by 6. Some outcomes appear slightly more often than others. In a game, this means the goblin hits harder more often than the math says.
gen_range avoids this trap. It implements rejection sampling. The method generates a raw number, checks if it falls within a range that divides evenly by your target size, and discards values that would skew the result. It retries until it finds a valid value. This guarantees uniform distribution. The performance cost is negligible. The safety is free.
Never roll dice with the percent operator. Use gen_range.
Reproducibility with Seeds
Sometimes you need the same random sequence every time. This happens during testing, debugging, or when you need to verify a simulation. thread_rng won't help here. It produces different results on every run. You need StdRng with a seed.
StdRng is a fast, deterministic generator. You control the starting state via a seed. The same seed always produces the same sequence. Use the SeedableRng trait to create a seeded instance.
use rand::{Rng, SeedableRng};
use rand::rand::rngs::StdRng;
fn simulate_battle(seed: u64) -> u32 {
// StdRng allows you to set a seed for reproducible results.
// This is essential for debugging and testing.
let mut rng = StdRng::seed_from_u64(seed);
let mut health = 100;
while health > 0 {
// Damage varies between 5 and 15.
let damage = rng.gen_range(5..=15);
health = health.saturating_sub(damage);
println!("Hit for {} damage. Health: {}", damage, health);
}
0 // Victory
}
fn main() {
// Running with the same seed produces identical output.
simulate_battle(12345);
}
Convention aside: use seed_from_u64 for simple seeds. For complex seeding, SeedableRng provides from_entropy and from_rng. Stick to seed_from_u64 unless you need cryptographic seeding.
Seeds turn chaos into a script. Use them to tame your randomness during development.
Pitfalls and Compiler Errors
The compiler catches common mistakes. Watch for these patterns.
If you forget to import the Rng trait, the compiler rejects your code with E0599 (no method named gen_range found). The gen_range method comes from the trait. Add use rand::Rng to your scope.
If you declare the generator without mut, the compiler rejects you with E0596 (cannot borrow as mutable). Random number generators mutate their internal state on every call. Always declare let mut rng.
Range boundaries trip people up. 1..10 excludes 10. 1..=10 includes 10. Use ..= for inclusive ranges like dice rolls. Use .. for half-open ranges like array indices.
Modulo bias is a silent killer. The compiler won't warn you. Your game balance will suffer. Use gen_range for all numeric random needs.
Don't fight the compiler here. Import the trait and mark the variable mutable.
Choosing Your Generator
Pick the tool that matches your need for control. Reproducibility saves hours of debugging.
Use rand::thread_rng() when you need a quick random number in application code and don't care about reproducibility. It's fast, cached, and auto-seeded.
Use StdRng with a seed when you are writing tests, debugging a simulation, or need deterministic behavior for verification. The same seed always yields the same sequence.
Use gen_range for almost all numeric random needs. It handles range boundaries and bias correction automatically. It accepts Range and RangeInclusive.
Use random::<T>() only for quick one-offs where you don't need a specific range, like generating a random boolean or a single byte. It calls thread_rng().gen::<T>() under the hood.
Use slice::shuffle when you need to randomize the order of a collection rather than generating individual numbers. It takes a generator and shuffles in place.
Reach for plain gen_range when lifetimes are simple. The unsafe alternative is rarely worth it.