Understanding the Error
Rust plays it safe. By default, every variable you create is immutable. This means once you bind a value to a name using the let keyword, that value is locked in. If you try to swap it out for something else later, the compiler will stop you cold.
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
3 | let x = 5;
| - first assignment to `x`
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
Think of this strictness as a safety net. It prevents a whole class of bugs where data changes unexpectedly behind your back. While it might feel restrictive at first, Rust gives you several idiomatic ways to work around it.
The Root Cause
The compiler throws E0384 because you are trying to change a value that was promised to be constant. In Rust, immutability is the baseline. Unless you explicitly tell the compiler a variable will change, it assumes the value stays the same for its entire stay in that scope. This allows the compiler to optimize your code more aggressively and makes your logic easier to follow.
Solution 1: Use the mut Keyword
The quickest path to a fix is declaring your variable as mutable. By adding mut, you signal to both the compiler and other developers that this value is meant to evolve.
Example: Tracking a Counter
fn main() {
// Use 'mut' to allow reassignment
let mut retry_count = 0;
println!("Current attempts: {}", retry_count);
retry_count = 1;
println!("Updated attempts: {}", retry_count);
}
When to use this: Opt for mut when you have a single piece of data that naturally changes over time. Common examples include loop counters, accumulators, or buffers that receive new data chunks.
Solution 2: Variable Shadowing
Sometimes you don't need a variable to be mutable; you just need to transform it. Shadowing lets you reuse a variable name by creating a brand-new binding with the let keyword. This effectively "shadows" the old variable, making it inaccessible in favor of the new one.
Example: Data Transformation
fn main() {
let input = " 42 ";
// Shadowing allows us to change the type from &str to usize
let input = input.trim();
let input: usize = input.parse().expect("Not a number!");
println!("The parsed number is: {}", input); // 42
}
Why shadowing is powerful:
- It lets you change the data type while keeping a descriptive name.
- The variable remains immutable after the final transformation. This prevents accidental changes later in your function.
Solution 3: Using Expressions for Assignment
A common trap for beginners is trying to initialize a variable inside an if/else block. Because if blocks in Rust are expressions, you don't need to declare a variable and then assign to it. You can simply return the value from the block directly.
The Wrong Way:
let status_code;
if success {
status_code = 200;
} else {
status_code = 404;
}
// If you try to change status_code here, you'll hit E0384
The Rust Way:
let is_authenticated = true;
// Bind the result of the entire 'if' expression to the variable
let status_code = if is_authenticated {
200
} else {
401
};
println!("Response: {}", status_code);
This pattern ensures status_code is only assigned once. It satisfies the compiler's immutability rules while keeping your code clean and functional.
Verification and Testing
Once you apply a fix, verify it with these steps:
- Run
cargo checkin your terminal. This validates your syntax and types without the overhead of a full build, saving you 5–10 seconds on larger projects. - If the check passes, execute
cargo runto confirm your logic works as intended. - Watch for "unused mut" warnings. If Rust tells you a variable doesn't need to be mutable, remove the
mutkeyword to keep your code safe.
Best Practices
To keep E0384 errors at bay, try these habits:
- Default to Immutability: Start every variable with
let. Only addmutwhen the compiler or your logic specifically demands it. - Leverage Match and If: Use
matchorifexpressions to calculate values. This is almost always cleaner than declaring a variable and updating it inside branches. - Keep Scopes Small: If a variable must be mutable, wrap that logic in a small block
{}or a helper function. This limits the area where the value can change, making debugging much easier.

