Skip to content
article

the borrow checker, explained to my ruby self

three errors every beginner hits, what they actually mean, and how i stopped fighting them

Mio Kobayashi@mio_rustacean

August 30, 2026 · 3 min read

i wrote ruby for five years before rust. in ruby every variable is a reference to an object somewhere, the garbage collector cleans up after you, and you never think about who "owns" anything. so my first month of rust was mostly me yelling at the compiler. here are the three errors i hit most, translated for past me.

if you want the 100 second version of rust first, fireship's is honestly good:

1. E0382: the value moved

let names = vec![String::from("mio"), String::from("emeka")];
let moved = names;
println!("{:?}", names);
// error[E0382]: borrow of moved value: `names`

in ruby, moved = names gives you two names for the same array. in rust, it moves ownership. a Vec owns heap memory, and rust wants exactly one owner so it knows exactly when to free it. after the move, names is not allowed anymore.

fixes: borrow it instead (let view = &names;) if you only need to read, or names.clone() if you really want two copies. clone is fine while learning. really.

2. E0502: reading and writing at the same time

let mut queue = vec![1, 2, 3];
let first = &queue[0];
queue.push(4);
println!("{first}");
// error[E0502]: cannot borrow `queue` as mutable because it is also borrowed as immutable

this one made me angry until i understood it. push may need more space, so the vec can move all its elements to a new place in memory. first would then point at the old place. in c that is a dangling pointer and a bad week. rust refuses to compile it.

the rule in one line: one writer OR many readers, never both at the same time. fixes: copy the value (let first = queue[0];, an i32 is Copy), or finish using first before you push. the compiler is smart about this, a borrow ends after its last use, not at the end of the block.

3. E0515: returning a reference to something that dies

fn longest_name(names: &[String]) -> &str {
    let mut best = String::new();
    for n in names {
        if n.len() > best.len() {
            best = n.clone();
        }
    }
    &best
    // error[E0515]: cannot return reference to local variable `best`
}

best lives inside the function. when the function returns, it is freed. a reference to it would point at nothing. the fix is to return a reference into the data the caller gave you, which lives long enough:

fn longest_name(names: &[String]) -> Option<&str> {
    names.iter().map(|n| n.as_str()).max_by_key(|n| n.len())
}

no lifetime annotations needed, rust figures out that the result borrows from names. and Option because the slice can be empty, which my ruby version silently returned "" for.

what helped me stop fighting

  • read the whole error. rust errors are long because they are useful. rustc --explain E0502 gives a full explanation with examples.
  • own your data first. use String in structs, not &str, until you have a reason. lifetimes in structs can wait.
  • clone when stuck, optimise later. a clone you can see is better than a bug you can't.

and because danny will ask: our old ruby service did p99 180 ms and 1.2 GB of memory. first we fixed an n+1 in the ruby code and it went to 60 ms. then the rust version: p99 11 ms, 40 MB. the n+1 was the bigger win, the rust part is real too 🦀❄️

Written by

Mio Kobayashi

rust backend @ a small logistics startup in sapporo. ex-ruby. they/them. snow, ramen, borrow checker 🦀❄️

Get the best of #types-go-brr weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

12

Sign in to join the conversation.

Sign in to comment
Danny Cho@danny_cho_la#4 in Types Go Brrr
I was going to ask, and you answered in the post. 180 to 60 from the N+1, 60 to 11 from Rust. That is an honest benchmark. Respect.
3 net votes
Mio Kobayashi@mio_rustacean#1 in Types Go Brrr
i knew you would 🙃 you trained me well
0 net votes
Emeka Nwosu@emeka_golang#4 in Types Go Brrr
Three errors to print a list of names. Rust people call this learning 😂 jokes aside, the E0502 explanation with the vec moving in memory is the first time it made sense to me
2 net votes
Ondřej Hrubý@rewrite_it_in_rust#4 in Types Go Brrr
good post. for beginners: also run cargo clippy, it explains half of these before the compiler does. and clone is fine, ignore anyone who says otherwise while you are learning 🦀
6 net votes
Karthik Subramaniam@karthi_rustacean#4 in Types Go Brrr
Small correction: 'borrow of moved value' is right for the println example, but if you passed names by value again it would say 'use of moved value'. Same E0382 code only. Good post overall.
1 net votes
Mio Kobayashi@mio_rustacean#1 in Types Go Brrr
oh good catch, thank you. noted 🦀
2 net votes
Frank Ostrowski@frank_cpp98#4 in Types Go Brrr
A compiler that refuses to let you return a dangling reference. In C++ we call that 'experience'. Regards.
2 net votes
Karl Lindqvist@kernel_karl#4 in Types Go Brrr
In C we call it a CVE number. I have three.
4 net votes
Mehmet Aydoğan@malloc_mehmet#4 in Types Go Brrr
C with a babysitter. But the babysitter is good at her job, I admit.
2 net votes
Olivia Martin@liv_codes#4 in Types Go Brrr
ok this is the first borrow checker explanation that didnt make me want to throw my laptop. 'one writer or many readers' is going on a sticky note fr
2 net votes
Max Petrosyan@max_2010#4 in Types Go Brrr
bro i tried rust after java for my mod tools and E0502 is literally all i see 😭 is it normal to clone everything at first
3 net votes
Mio Kobayashi@mio_rustacean#1 in Types Go Brrr
yes!! clone everything. fix it later. nobody's first rust is pretty, mine wasn't 🦀
0 net votes