The Problem: Multiple References Gone Wrong
Let me show you a common problem that JavaScript developers face every dayβbut don't always realize it's a problem.
The JavaScript Scenario
Imagine you have an array, and you create multiple references to it:
const x = [1, 2, 3]
const y = x // another reference to same array
const z = x // and another
function sleep(ms) {
return new Promise((res) => setTimeout(res, ms))
}
function* geny() {
let i = 0
while (1) {
yield y[i] // iterating through y
i = (i + 1) % 3
}
}
const iterY = geny()
async function displayData() {
while (1) {
await sleep(1000)
console.log(iterY.next())
}
}
// After 5 seconds, mutate the array through z
setTimeout(() => { z[0] = 90 }, 5000)
displayData()
The Output
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
// ... 5 seconds pass ...
{ value: 90, done: false } // π± SURPRISE! The value changed!
{ value: 2, done: false }
{ value: 3, done: false }
What Went Wrong?
The problem is subtle but dangerous:
-
Multiple references to the same data (
x,y,zall point to[1, 2, 3]) -
Mutation through one reference (
z[0] = 90) silently affects all others - No way to track where the mutation came from
-
Hard to reason about β the code iterating through
ysuddenly sees different values
This is called aliasing and mutation β a notoriously difficult bug to track down in large codebases.
The Rust Solution: Borrowing
Rust prevents this problem entirely by enforcing strict borrowing rules at compile time. Let's see how:
Rust's Approach
fn main() {
let mut x = vec![1, 2, 3];
// This creates an immutable borrow
let y = &x;
// β ERROR: Cannot borrow as mutable while y exists!
// let z = &mut x;
// z[0] = 90;
println!("{:?}", y); // y is still in use here
}
The compiler says: NO! β
error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
The Borrowing Rules (Rust's Safety Guarantee)
Rust enforces these rules:
| Scenario | Allowed? | Why? |
|---|---|---|
Multiple immutable borrows (&x, &x, &x) |
β YES | Readers never conflict |
One mutable borrow (&mut x) |
β YES | Single writer, no conflicts |
Immutable + Mutable (&x and &mut x) |
β NO | Writer and readers = data race |
Correct Rust Code
If you want to mutate the data, you must release all other references first:
fn main() {
let mut x = vec![1, 2, 3];
// Immutable borrow
let y = &x;
println!("y: {:?}", y);
// y is no longer used, borrow ends here
// Now we can create a mutable borrow
let z = &mut x;
z[0] = 90;
println!("z: {:?}", z);
}
Output:
y: [1, 2, 3]
z: [90, 2, 3]
Comparing the Languages
JavaScript: Trust and Hope π
const x = [1, 2, 3]
const y = x
const z = x
z[0] = 90
console.log(y) // [90, 2, 3] β Surprise! You have to remember to check
Relies on:
- Developer discipline
- Code reviews
- Runtime testing
- Hope that you didn't miss anything π
Rust: Compile-Time Guarantees π¦
let mut x = vec![1, 2, 3];
let y = &x;
let z = &mut x; // β Compiler error BEFORE runtime!
z[0] = 90;
Guarantees:
- Checked at compile time
- No runtime surprises
- Clear ownership semantics
- Safe by default
Why This Matters
JavaScript Problems (Real-World Scenarios)
- Shared state bugs β State changes in unexpected places
- Iterator invalidation β Data changes while iterating
- Race conditions β Concurrent modifications
- Difficult debugging β Hard to trace where mutation happened
Rust Solutions
// Rust FORCES you to be explicit about access patterns
// Read-only access (shared)
let reader1 = &data;
let reader2 = &data;
let reader3 = &data;
// All readers are safe!
// Exclusive access (mutation)
let writer = &mut data;
// Only one writer, guaranteed no conflicts
Key Takeaways
β JavaScript allows multiple mutable references β flexible but error-prone
β Rust prevents multiple mutable references β strict but safe
β The JavaScript code showed a *real problem* that Rust's borrowing solves
β Borrowing is not a limitation β it's a feature that catches bugs at compile time
β Trade-off: Less flexibility for more safety and predictability
The Bigger Picture
This is why Rust is gaining traction in systems programming:
- Memory safety without garbage collection
- No runtime overhead for safety checks
- Concurrency without fear (data races are impossible)
- Performance + Reliability together
While JavaScript's flexibility is great for rapid development, Rust's borrowing system is invaluable for:
- Large codebases
- Concurrent systems
- Performance-critical code
- Safety-critical applications
Try It Yourself
Want to experiment?
JavaScript: Run the code in your browser console and watch the mutation happen silently. TryTheSample
Rust: Head to play.rust-lang.org and try violating the borrowing rulesβthe compiler will teach you! π¦
fn main() {
let mut data = vec![1, 2, 3];
let immutable_ref = &data;
let mutable_ref = &mut data; // Try this!
mutable_ref[0] = 99;
}
Conclusion
Rust's borrowing system might seem restrictive at first, but it solves real problems that JavaScript developers face daily. By catching aliasing-and-mutation bugs at compile time, Rust gives you confidence that your code is correct before it ever runs.
The JavaScript example above isn't a flaw in JavaScriptβit's a feature. The flaw is when that feature leads to subtle bugs. Rust simply chose a different trade-off: less flexibility, zero surprises.
Happy coding! π¦β¨
Have you encountered aliasing-and-mutation bugs in your code? Share your war stories in the comments! And if you're curious about Rust, the borrowing rules might just be the best investment you make in understanding systems programming.
Top comments (0)