DEV Community

Timevolt
Timevolt

Posted on

Rust Ownership System Explained for JavaScript Developers

Rust Ownership System Explained for JavaScript Developers

Quick context (why you're writing this)

I was trying to rewrite a small utility I’d written in JavaScript—a function that takes a string, splits it into words, and returns the longest one. In JS it’s trivial: you pass the string around, mutate arrays, and nothing blows up. When I attempted the same thing in Rust, the compiler kept yelling at me about “use of moved value” and “cannot borrow as mutable because it is also borrowed as immutable”. I spent a good chunk of an afternoon staring at those errors, thinking I’d missed some syntax detail, only to realize the real issue was a completely different way of thinking about data. If you’ve ever felt that Rust’s compiler is being overly pedantic, you’re not alone—but once you grasp what it’s protecting you from, the frustration turns into appreciation.

The Insight

Rust doesn’t treat variables like JavaScript’s loosely‑typed references. Instead, it enforces ownership at compile time. Three ideas tend to surprise developers coming from a garbage‑collected world:

  1. Move semantics – assigning a value to another variable moves it; the original is no longer usable unless you explicitly clone it.
  2. Borrowing rules – you can have either many immutable references or exactly one mutable reference to a piece of data, but never both at the same time.
  3. Lifetimes – the compiler tracks how long references are valid, preventing dangling pointers without a garbage collector.

The first two are the ones that trip people up most often, and they directly address the class of bugs JavaScript developers know all too well: accidental shared‑state mutations and use‑after‑free‑like mistakes (though in JS they show up as weird undefined values rather than crashes).

Let’s look at each with a concrete example, show the common mistake, and then see how to do it right.

How (with code)

Move semantics – the “you can’t use it after you give it away” surprise

fn main() {
    let greeting = String::from("Hello, world!");
    let another = greeting; // greeting is moved into another
    println!("{}", greeting); // <-- compile error: use of moved value
}
Enter fullscreen mode Exit fullscreen mode

If you come from JavaScript, you’d expect greeting to still hold the string because variables are just references. In Rust, String owns its heap allocation, and assigning it transfers that ownership. The compiler treats greeting as invalid after the move, preventing you from accidentally reading freed memory.

What you’d likely do wrong: try to work around the error by cloning everywhere, which defeats the purpose of zero‑cost abstractions.

The idiomatic fix: either clone when you truly need two independent owners, or restructure the code so you only need one owner at a time.

fn main() {
    let greeting = String::from("Hello, world!");
    let another = greeting.clone(); // explicit copy, now both are valid
    println!("{}", greeting);
    println!("{}", another);
}
Enter fullscreen mode Exit fullscreen mode

Or, if you only need to read the data, borrow it instead of taking ownership:

fn main() {
    let greeting = String::from("Hello, world!");
    let len = calculate_length(&greeting); // &greeting is an immutable borrow
    println!("The length is {}", len);
    println!("{}", greeting); // still usable because we only borrowed
}

fn calculate_length(s: &str) -> usize {
    s.len()
}
Enter fullscreen mode Exit fullscreen mode

Borrowing rules – the “no mutable aliasing while someone’s reading” surprise

JavaScript lets you have multiple variables pointing to the same object and modify them freely. Rust says “nope” when you try to mutate while someone else holds an immutable view.

fn main() {
    let mut vec = vec![1, 2, 3];
    let slice = &vec; // immutable borrow
    vec.push(4);      // error: cannot borrow mutably because it is also borrowed as immutable
    println!("{:?}", slice);
}
Enter fullscreen mode Exit fullscreen mode

The compiler blocks the push because the immutable slice could be used later, and mutating the vector might invalidate that reference (e.g., if the vector needs to reallocate). This prevents data races and iterator invalidation bugs that are painfully common in JS when you accidentally mutate an array while iterating over it.

Typical mistake: reaching for RefCell or Mutex to get around the error without understanding why it exists, ending up with runtime panics or unnecessary overhead.

Better approach: limit the scope of the borrow, or mutate first and then take the immutable view.

fn main() {
    let mut vec = vec![1, 2, 3];
    vec.push(4); // mutate while we have exclusive access
    let slice = &vec; // now we can safely take an immutable reference
    println!("{:?}", slice);
}
Enter fullscreen mode Exit fullscreen mode

If you truly need simultaneous read‑and‑write access in different parts of your code, consider splitting the data into distinct pieces or using interior mutability primitives like RefCell (for single‑threaded scenarios) or Mutex (for threads) only after you’ve confirmed that the borrowing rules are genuinely too restrictive for your design.

Practical use case: building a tokeniser

Imagine you’re writing a simple tokenizer that takes a source string, splits it into words, and returns a vector of tokens. In JavaScript you might do:

function tokenise(src) {
    return src.trim().split(/\s+/);
}
Enter fullscreen mode Exit fullscreen mode

In Rust, the naive version looks similar but runs into ownership issues if you try to keep the original string around while also returning tokens that borrow from it.

fn tokenise(src: &str) -> Vec<&str> {
    src.trim().split_whitespace().collect()
}
Enter fullscreen mode Exit fullscreen mode

This works because the returned vector holds references (&str) that borrow from src. The caller must ensure src lives at least as long as the tokens—a lifetime the compiler tracks for you. If you tried to return String tokens instead, you’d end up cloning each slice, which is fine but less efficient.

If you mistakenly tried to store the tokens in a struct that outlives the source string, the compiler would stop you:

struct Tokeniser {
    src: String,
    tokens: Vec<&str>,
}

impl Tokeniser {
    fn new(s: String) -> Self {
        let tokens = s.trim().split_whitespace().collect(); // error: s does not live long enough
        Tokeniser { src: s, tokens }
    }
}
Enter fullscreen mode Exit fullscreen mode

The fix is either to own the tokens (Vec<String>) or to tie the struct’s lifetime to the source string using explicit lifetime annotations—a topic for another day, but the key takeaway is that the compiler forces you to decide up front whether you’re borrowing or owning.

Why This Matters

Understanding ownership isn’t just about appeasing the borrow checker; it reshapes how you reason about state. When you know that a value can only have one mutable owner at a time, you start writing functions that are easier to test in isolation because they don’t hide hidden dependencies. You also get a natural guard against data races: if multiple threads try to access the same data, the type system will only let them do so through synchronized primitives, making concurrent Rust code feel less like a minefield.

For a JavaScript developer, the payoff is fewer “it works on my machine” moments. The bugs that used to surface as undefined values or weird UI glitches become compile‑time errors you fix before the code even runs. And because Rust’s model is explicit, you inevitably write code that’s clearer about who is responsible for cleaning up resources—a habit that translates back to better resource management in any language, even if you go back to JS for a quick prototype.

Mastering these concepts also makes you a better communicator on teams. You’ll be able to explain why a certain API takes &mut self instead of &self, or why a function returns an owned String rather than a slice, and your teammates will appreciate the transparency.

Challenge

Take a small piece of JavaScript you’ve written recently—maybe a utility that filters an array based on some condition—and rewrite it in Rust using iterators and borrowing. Notice where you want to clone data versus where you can just borrow. Pay attention to the compiler’s messages; they’re not roadblocks, they’re guides to clearer ownership.

Give it a try, and drop a link to your gist or a snippet in the comments. I’m curious to see where you hit friction and how you solved it. Happy coding!

Top comments (0)