DEV Community

Timevolt
Timevolt

Posted on

Ownership in Rust: A JavaScript Dev's Quest Like Neo in The Matrix

The Quest Begins (The "Why")

I still remember the first time I tried to port a little Node.js utility to Rust. I was confident—I knew callbacks, promises, and the event loop like the back of my hand. I wrote a function that grabbed a user object, tweaked a field, and returned it. In JavaScript it looked something like this:

function updateUser(user) {
  user.lastLogin = Date.now();
  return user;
}
Enter fullscreen mode Exit fullscreen mode

I called it, passed the same object around, and everything seemed fine—until I started mutating the returned value in two different places and watched the original data change behind my back. Classic shared‑state bug. I spent an hour staring at console.log output, feeling like I was stuck in a loop, wondering why my “pure” function wasn’t pure at all.

That frustration was the dragon I needed to slay. I wanted a language that would stop me from shooting myself in the foot before I even ran the code. Rust’s ownership system promised exactly that, but the first few pages felt like reading ancient runes. I kept thinking, “Surely I can just treat &mut like a JavaScript reference?” Spoiler: I couldn’t, and the compiler would lovingly remind me why.

The Revelation (The Insight)

When the concepts finally clicked, it felt like finally beating the final boss in Dark Souls—hard, but incredibly satisfying. Here are the two surprising pieces that most JavaScript developers miss at first glance, and why they matter.

1. Move Semantics – Variables Don’t Get Copied, They Get Moved

In JavaScript, assigning an object to another variable or passing it to a function just copies the reference. The underlying data lives in one place, and both variables can reach it. Rust works differently: ownership moves unless you explicitly copy or borrow.

let s1 = String::from("hello");
let s2 = s1; // s1 is moved into s2
println!("{}", s1); // ❌ compile‑error: value borrowed after move
Enter fullscreen mode Exit fullscreen mode

The gotcha? After the move, s1 is considered invalid. Trying to use it is a compile‑time error, not a runtime surprise. This eliminates entire classes of bugs where you accidentally mutate something you thought you owned exclusively.

2. Borrowing – Immutable & Mutable References Coexist with Rules

If you need to let another part of your code read data without taking ownership, you borrow it. Rust lets you have any number of immutable references (&T) or exactly one mutable reference (&mut T), but never both at the same time.

let mut data = vec![1, 2, 3];
let r1 = &data; // immutable
let r2 = &data; // fine, another immutable
// let r3 = &mut data; // ❌ cannot borrow as mutable while immutable exists
Enter fullscreen mode Exit fullscreen mode

If you violate this rule, the compiler stops you before you can even run the program. This is how Rust guarantees data‑race freedom at compile time—a guarantee JavaScript engines can only hope to approximate with runtime checks or strict discipline.

Why These Features Exist

Both rules stem from a single goal: ensure that there is exactly one owner responsible for cleaning up a piece of memory, and that no other part of the program can unexpectedly mutate it while it’s being used. In JavaScript, the garbage collector eventually cleans up unreachable objects, but it gives you no compile‑time guarantees about who may change what when. Rust shifts that reasoning to compile time, turning many runtime headaches into compile‑time errors you can fix once and for all.

Wielding the Power (Code & Examples)

The Struggle: JavaScript’s Shared Mutable State

Imagine a simple game where each player has a score, and we want to give a bonus to the leading player without accidentally altering the original scores array.

function giveBonus(scores) {
  const leader = scores.reduce((a, b) => (a.score > b.score ? a : b));
  leader.score += 50; // mutates the original object inside the array!
  return scores;
}

const players = [
  { name: "Ada", score: 120 },
  { name: "Bob", score: 95 },
];
console.log(giveBonus(players)); // Ada's score changed in the original array
Enter fullscreen mode Exit fullscreen mode

If another part of the code later reads players, it sees the boosted score—maybe not what we intended.

The Victory: Rust’s Ownership in Action

Now let’s write the same logic in Rust, using ownership and borrowing to make the intent explicit and safe.

#[derive(Debug)]
struct Player {
    name: String,
    score: u32,
}

fn give_bonus(mut players: Vec<Player>) -> Vec<Player> {
    // Find the player with the highest score (takes ownership of the vector)
    if let Some(leader) = players.iter_mut().max_by_key(|p| p.score) {
        leader.score += 50; // mutable borrow of the leader only
    }
    players // ownership moved back to the caller
}

fn main() {
    let roster = vec![
        Player { name: "Ada".into(), score: 120 },
        Player { name: "Bob".into(), score: 95 },
    ];

    let updated = give_bonus(roster);
    // `roster` is now invalid; we cannot use it again
    println!("{:?}", updated);
}
Enter fullscreen mode Exit fullscreen mode

What changed?

  1. Move of the whole vectorgive_bonus takes ownership of players. After the call, roster can’t be used, preventing accidental reuse of the old data.
  2. Mutable borrow of a single element – We get a &mut Player only for the leader, adjust its score, and the borrow ends when the function returns. No other part of the code can mutably or immutably alias that element while we’re adjusting it.
  3. Explicitness – The signature tells anyone reading the code: “this function may modify the vector you give me.” If you want a read‑only version, you’d take &[Player] instead, and the compiler would enforce that you don’t modify anything.

Common Traps & How to Avoid Them

Trap What Happens Fix
Using a value after moving it let a = vec![1,2,3]; let b = a; println!("{:?}", a); → compile error Clone if you really need a second copy (let b = a.clone();) or redesign to avoid the second use.
Mutably borrowing while an immutable borrow exists let r1 = &v; let r2 = &mut v; → compile error Scope the immutable borrow ({ let r1 = &v; }) or restructure so you don’t need overlapping borrows.
Returning a reference to local data fn foo() -> &String { let s = String::from("hi"); &s } → compile error Return the owned String (fn foo() -> String { String::from("hi") }) or use a static/lifetime‑elided reference if the data truly lives longer.

Each of these “gotchas” is the compiler protecting you from a class of bugs that would otherwise only surface at runtime—often in production, under load, and at the worst possible moment.

Why This New Power Matters

Mastering ownership does more than make your Rust code compile; it rewires how you think about state in any language. You start asking:

  • Who owns this piece of data?
  • For how long do I need to read or mutate it?
  • Can I guarantee that no one else will touch it while I’m working on it?

When you bring that mindset back to JavaScript, you’ll find yourself reaching for const more often, copying objects deliberately with spread syntax or structuredClone, and being wary of hidden shared state in callbacks or event handlers. You’ll write functions that are easier to reason about, easier to test, and far less prone to the “it worked on my machine” syndrome.

In short, ownership gives you a superpower: the ability to catch logical errors before you run the code. It’s like having a linter that understands the semantics of your program, not just its syntax.

Your Turn

Here’s a small challenge: take a JavaScript function that manipulates a nested object (maybe a game state or a UI tree) and rewrite it in Rust using ownership and borrowing. Notice where you have to clone, where you can borrow, and how the compiler guides you toward a safer design.

What surprised you the most about Rust’s ownership system? Did any of the “gotchas” feel familiar from a bug you’ve chased down before? Drop your thoughts in the comments—I’d love to hear how this quest changed your coding style! 🚀

Top comments (0)