Ownership and Borrowing

Lesson 1 of 5

Moves, Copies and Drop

"Linear types can change the world!" — Philip Wadler, Programming Concepts and Methods (1990)

Part I spent five chapters on the machine and never once taught you to write Rust. That was deliberate, and this is the chapter it was building towards. Everything the compiler is about to refuse you has a mechanical reason you have already seen: a Vec is a twenty-four-byte header pointing at a heap buffer, a move copies the header and not the buffer, and the compiler inserts the call that frees that buffer at the point it proves the value dies. Ownership is the rule that makes the last part possible.

State it plainly. Every value in Rust has exactly one owning binding. When that binding goes out of scope, the value is dropped. Assigning or passing the value transfers the ownership and ends the original binding's claim. Three sentences, and the rest of Part II is their consequences.

What makes this different from a convention is that the compiler tracks it. Not at run time — there is no flag on the value, no reference count, nothing to inspect while the program runs. The analysis happens during compilation, and the result is either a program where every value is freed exactly once or no program at all. That is the trade Rust offers in place of a garbage collector, and the price is that the compiler sometimes says no.

This chapter is about the first of those refusals and the machinery around it: what exactly a move is, why some types copy instead, what Drop means and why it cannot coexist with Copy, and what happens when you move only part of a thing. Along the way you will meet the error you will see more than any other in your first month — E0382, borrow of moved value — and it is worth meeting it deliberately, read properly, while you have time to study it rather than while you are trying to ship something.

A note on these blocks. Several of them do not compile, on purpose, and that is not a defect in the text. In Rust the compiler's refusal is frequently the entire lesson, so this volume checks its rejections the same way it checks its programs: a block marked to fail is compiled, and the build breaks if it succeeds or fails with a different error than the one claimed. When you read E0184 below, that code was produced by rustc and not by me.

The big question

If the compiler can prove at build time that every value is freed exactly once, what has it had to forbid you from writing in order to prove it?

The refusal, read properly

Here is the canonical mistake, and the error it produces is worth reading line by line rather than skimming.

#[derive(Debug)]
struct Receipt { id: u32, note: String }

fn file_away(r: Receipt) -> u32 { r.id }

fn main() {
    let receipt = Receipt { id: 7, note: String::from("coffee") };
    let id = file_away(receipt);
    println!("filed {id}");
    println!("{:?}", receipt);
}
error[E0382]: borrow of moved value: `receipt`
  --> src/main.rs:10:22
   |
 7 |     let receipt = Receipt { id: 7, note: String::from("coffee") };
   |         ------- move occurs because `receipt` has type `Receipt`, which does not implement the `Copy` trait
 8 |     let id = file_away(receipt);
   |                        ------- value moved here
 9 |     println!("filed {id}");
10 |     println!("{:?}", receipt);
   |                      ^^^^^^^ value borrowed here after move
   |
note: consider changing this parameter type in function `file_away` to borrow instead if owning the value isn't necessary

Four pieces of information, and each is doing real work.

It names where the value came from and why a move happened at all — Receipt does not implement Copy, so passing it transfers ownership rather than duplicating it. It names where the move occurred, the argument position on line 8. It names where you then tried to use it, line 10. And it proposes a fix that is usually the right one: if file_away did not need to own the receipt, it should have taken a reference.

That last suggestion is the one to internalise, because it points at the real mistake. The error is not that line 10 is wrong. The error is that file_away asked for ownership of a whole Receipt in order to read one u32 out of it — a signature that takes more than it needs. Most E0382s in real code are a function signature problem wearing a call-site costume.

Notice also what the compiler did not do. It did not copy the receipt to be helpful, and it did not reference-count it. It stopped. The single-owner property is not a default that can be overridden by convenience; it is the thing being proved.

Everyday analogy

Consider handing someone the only physical copy of a contract. Once it is in their hands you cannot also have it — not because of a rule somebody enforces, but because there is one object and it is over there. If you need it back they must hand it back; if you only wanted to read a clause you should have let them look over your shoulder instead. What feels strange about Rust is not this logic, which everybody already understands about physical things. It is that most programming languages have trained us to expect assignment to behave like a photocopier, silently and for free, and Rust declines to pretend that copying a heap-backed structure is free.

When assignment copies instead

Some types do duplicate on assignment, and the rule for which ones is precise rather than intuitive.

// Copy is a promise: duplicating these bytes produces a valid second value.
#[derive(Debug, Clone, Copy)]
struct Point { x: i32, y: i32 }

// No Copy: this owns a heap buffer, so a bitwise duplicate would mean two owners.
#[derive(Debug, Clone)]
struct Label { text: String }

fn main() {
    let p = Point { x: 1, y: 2 };
    let q = p;                       // copies
    println!("both alive: ({},{}) ({},{})", p.x, p.y, q.x, q.y);

    let a = Label { text: String::from("north") };
    let b = a.clone();               // explicit, allocates
    println!("cloned: {} {}", a.text, b.text);

    let c = b;                       // moves; b is finished
    println!("moved:  {}", c.text);

    println!("p survived the copy: {:?}", p);
}
both alive: (1,2) (1,2)
cloned: north north
moved:  north
p survived the copy: Point { x: 1, y: 2 }

Point is Copy, so let q = p duplicates eight bytes and leaves p entirely usable. Label is not, so let c = b moves and b is finished — identical syntax, opposite behaviour, decided by the type.

The rule behind Copy is worth stating exactly, because "small types are Copy" is the folk version and it is wrong. A type may be Copy only if duplicating its bytes produces a second valid, independent value. An i32 qualifies: two copies of the same four bytes are two perfectly good integers. A String does not, and not because it is large — because its bytes include a pointer to a heap buffer, and duplicating them would produce two values that both believe they own that buffer. When both went out of scope, the buffer would be freed twice.

So Copy is not an optimisation and not a size threshold. It is a claim about whether the single-owner invariant survives duplication. The [0u8; 4096] from Part I is Copy despite being four kilobytes, because duplicating four kilobytes of plain bytes is valid, merely wasteful. Rust is protecting correctness, not your performance.

Clone is the escape hatch for everything else: an explicit, possibly expensive, user-defined duplication. a.clone() allocated a second buffer and copied the text into it. The language deliberately makes you write the word, so the cost is visible at the call site — and #[derive(Clone)] just wires each field's clone together.

Where this came from

The idea is older than Rust and comes from linear logic, introduced by Jean-Yves Girard in 1987: a logic in which each assumption must be used exactly once, rather than freely duplicated or discarded. Philip Wadler brought it into programming languages with the paper this chapter's epigraph comes from, arguing that types tracking single use could manage resources — files, memory, handles — without a collector. For two decades this was largely an academic line of work with a reputation for being too restrictive to program in. Rust's contribution was not inventing the idea but making it ergonomic enough to ship: borrowing, which Part II's next chapter covers, is precisely the concession that keeps a linear system usable, because it lets you look at a value without consuming it.

Drop, and the rule it forces

Drop is the other half of ownership. If exactly one binding owns a value, then exactly one scope ending can clean it up, and Drop is where you say what cleaning up means.

Part I showed drop order — reverse of declaration, deterministic, at a point you can see. What it did not show is the constraint Drop places on the type system.

#[derive(Clone, Copy)]
struct Handle { id: u32 }

// A type cannot be both Copy and Drop. Copy says "duplicating the bytes is
// fine"; Drop says "cleanup must run exactly once per value". Together they
// would mean running cleanup once per silent duplicate.
impl Drop for Handle {
    fn drop(&mut self) {
        println!("closing handle {}", self.id);
    }
}

fn main() {
    let h = Handle { id: 1 };
    println!("{}", h.id);
}
error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor
 --> src/main.rs:3:8
  |
2 | #[derive(Clone, Copy)]
  |                 ---- in this derive macro expansion
3 | struct Handle { id: u32 }
  |        ^^^^^^ `Copy` not allowed on types with destructors
  |
note: destructor declared here

Handle is four bytes of plain integer, so by the folk rule it should obviously be Copy. The compiler refuses, and the reason is a genuine contradiction rather than a technicality.

Copy means assignment silently duplicates. Drop means cleanup runs once per value when it goes out of scope. Allow both and a single let b = a; creates a second value whose scope will also end, so drop runs twice — and if drop closes a file descriptor or releases a lock, it closes it twice. That is exactly the double-free class of bug the whole ownership system exists to eliminate, so the type system forbids the combination at the definition rather than catching it later.

The practical reading: a type that manages a resource cannot be Copy. File handles, sockets, locks, allocations, database connections — all of them move, none of them duplicate, and that is not an inconvenience but the mechanism by which their cleanup is guaranteed to happen once.

This is also why Drop on a type you are writing is a bigger decision than it looks. It changes the type's position in the system: no Copy, restrictions on moving fields out, and cleanup that will definitely run, including during a panic unwind.

Moving part of a thing

Ownership is tracked per place, not per variable, which produces a behaviour that surprises people and is extremely useful once understood.

#[derive(Debug)]
struct Order { id: u32, customer: String, notes: String }

fn main() {
    let order = Order {
        id: 11,
        customer: String::from("ada"),
        notes: String::from("gift wrap"),
    };

    // A partial move: `customer` leaves, the rest of `order` stays valid.
    let who = order.customer;
    println!("customer   {who}");
    println!("id still   {}", order.id);
    println!("notes still {}", order.notes);

    // But `order` as a WHOLE can no longer be used — it is incomplete.
    // println!("{:?}", order);   // <- uncomment to see E0382
}
customer   ada
id still   11
notes still gift wrap

Moving order.customer out did not invalidate order entirely. The compiler knows precisely which field left, so order.id and order.notes remain perfectly usable — it is only order as a whole that has become unusable, because one of its fields is gone and the struct is no longer complete.

That precision is what makes the system bearable in practice. A coarser analysis would mark the whole variable dead at the first partial move and force you to destructure everything up front. Instead the compiler tracks each field separately, which is also why destructuring in a let or a match can take ownership of some fields while leaving others.

Two limits are worth knowing. You cannot partially move out of a type that implements Drop, because drop takes &mut self and would then be handed an incomplete value. And you cannot move out of something you only borrowed, or out of an index into a Vec, because the collection would be left with a hole — which is what std::mem::take and Vec::remove exist to handle properly.

The engineer's notebook

The thing I had to unlearn here was reading E0382 as a complaint about the line it points at. It almost never is. In the first block, line 10 is innocent — the defect is on line 4, where file_away asked to own a whole Receipt in order to read a u32. I spent an embarrassing amount of my first Rust month "fixing" these errors at the point of use, by cloning, and the code got slower and no clearer. The habit that actually works is to read the error bottom-up: the compiler's suggested fix names the function, and the function signature is usually where the mistake is.

Where moves happen that you did not write

Moves are not only caused by let and by function arguments. Two common constructs move things, and both catch people.

struct Job(String);

fn run(j: Job) -> String { j.0 }

fn main() {
    let jobs = vec![Job(String::from("a")), Job(String::from("b"))];

    // `into_iter` hands you owned values; the Vec is consumed.
    for j in jobs {
        println!("ran {}", run(j));
    }
    // `jobs` is gone here — it was moved into the loop.

    let name = String::from("auditor");
    // A `move` closure takes ownership of what it captures.
    let announce = move || format!("hello from {name}");
    println!("{}", announce());
    println!("{}", announce());   // Fn, so callable repeatedly

    // Captured by value, so `name` is no longer usable out here.
    let counted = vec![1, 2, 3];
    let total: i32 = counted.iter().sum();      // borrows
    println!("still usable: {counted:?} sum {total}");
}
ran a
ran b
hello from auditor
hello from auditor
still usable: [1, 2, 3] sum 6

for j in jobs consumed the vector. Writing for over a collection by value calls into_iter, which hands out owned elements and takes the collection with it — so jobs is unusable after the loop. Write for j in &jobs instead and you iterate over references, borrowing rather than consuming, which the last two lines of the block show with counted.

The move closure is the second case. move tells the closure to capture by value, so name was transferred into it and cannot be used outside afterwards. This is exactly what you want when the closure outlives the current scope — a thread, a callback, a task — and exactly what you do not want when you only meant to read the value.

Note that the closure is callable twice. Capturing by value does not make it single-use; it implements Fn because calling it only reads the captured String. A closure that consumed what it captured would implement FnOnce and could be called once, which is the type system noticing the same ownership rule one level up.

That gives the three closure traits their meaning, and they are ownership rules rather than arbitrary categories. FnOnce is implemented by every closure: it can be called at least once, and if calling it moves something out of its captures, once is all you get. FnMut is for closures that mutate what they captured, so they need &mut self and cannot be called from two places at the same moment. Fn only reads, so it can be called repeatedly and shared freely. The compiler picks the most permissive one your body qualifies for, which is why you rarely write them on closures and frequently read them in signatures — a function taking FnOnce is telling you it will call your closure exactly once, and that is a promise about ownership, not about arity.

In the real world

The commonest production shape of this is a function that takes String where &str would do, or a Vec<T> where &[T] would do. It compiles, it works, and it quietly forces every caller either to give up their data or to clone it — so the cost lands on code the author never sees, usually as a .clone() somebody added to make the error go away. Reviewers who know this check parameter types first when they see clones appearing in a diff, because the clone is the symptom and the signature is the disease. The rule of thumb that survives contact with real codebases: take ownership only when you will store it, return it, or consume it.

Getting a value out of a place you do not own

One situation comes up constantly and has a standard answer that is not obvious: you have a mutable reference to something, and you need to take a value out of it.

The compiler forbids the direct approach, and for the reason the whole chapter has been building towards. If you could move a String out of a struct you only borrowed, the struct would be left with a field containing nothing — and the owner, who still believes their struct is intact, would eventually drop it and run cleanup on a value that is not there. The borrow does not grant you the right to leave a hole.

What you can do is exchange. The standard library provides three functions in std::mem for this, and they are the same idea at different levels of convenience.

std::mem::replace(&mut place, new_value) puts new_value into the place and hands you back what was there. Nothing is ever missing, so the invariant holds throughout: before the call the place holds the old value, after it the place holds the new one, and you own the old one. This is the general tool.

std::mem::take(&mut place) is the same thing where the replacement is the type's default — an empty String, an empty Vec, a zero. It requires the type to implement Default, and it is the one you will reach for most, because "take this and leave an empty one" is overwhelmingly the common case. It turns a method that wanted to consume a field into one that can work through &mut self.

std::mem::swap(&mut a, &mut b) exchanges two values in place, owning neither. It is the right tool when both sides matter — rotating buffers, for instance, where you want the old one back rather than a default.

Option has its own version of the same move, and it is the most expressive of the four. opt.take() replaces an Option with None and returns what was there, so a field declared Option<Connection> can hand out its connection and visibly record that it no longer has one. That pattern — making "absent" a representable state rather than an accident — is how you model a resource that is legitimately removable, and it is far better than a sentinel value or a half-initialised struct.

The reason to learn these early is that they are the correct answer to a large family of borrow-checker arguments. When you find yourself wanting to write self.buffer into a function that consumes it, the question is not how to defeat the compiler; it is whether you want to leave an empty buffer behind, swap in a fresh one, or record the absence in the type. All three are one line, and each says something different to the next reader.

Common Mistakes

  1. Reading E0382 as an error on the line it points at. The use-after-move is the symptom; the cause is almost always a function signature that asked for ownership it did not need. Read the compiler's suggested fix, which names the function.

  2. Reaching for .clone() to make the error go away. It compiles and it allocates, and you have paid for a copy to work around a signature you could have changed. Clone when you genuinely need a second independent value, not to quiet the borrow checker.

  3. Believing small types are Copy. Size is irrelevant. Copy is allowed only when duplicating the bytes yields a valid independent value, which is why a four-kilobyte array qualifies and a twenty-four-byte String does not.

  4. Expecting to add Copy to a type with a destructor. It is E0184 and the contradiction is real: Copy duplicates silently, Drop runs once per value, so together they would run cleanup once per invisible duplicate.

  5. Writing for x in collection when you meant to borrow. That form calls into_iter and consumes the collection. &collection iterates by reference and leaves it usable, which is what you wanted nine times out of ten.

  6. Adding move to a closure reflexively. move captures by value and ends the original binding. Use it when the closure outlives the scope — threads, tasks, callbacks — not as decoration.

  7. Assuming a partial move kills the whole struct. It does not: the fields that did not move stay usable. Only the struct as a whole becomes unusable, and knowing that saves a lot of unnecessary destructuring.

  8. Trying to move a field out of a type that implements Drop. The compiler forbids it, because drop would later receive an incomplete value. std::mem::take or restructuring the type are the ways through.

  9. Implementing Drop without noticing what it costs. It rules out Copy, restricts moving fields out, and adds cleanup that runs on panic unwind too. Worth it for a real resource, needless ceremony for a plain data struct.

  10. Treating ownership as a run-time mechanism. There is no flag on the value and nothing to inspect while running. The entire analysis is at compile time, which is why it costs nothing and also why the compiler must sometimes refuse a program that would in fact have been fine.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Warm-up
What happens when you pass a String to a function by value, and why can you not use the original afterwards?
Model answer — The String is moved: its three-word header — pointer, length and capacity — is copied into the parameter, and the original binding is marked as no longer usable by the compiler. The heap buffer is not copied and not touched. You cannot use the original because that would mean two bindings believing they own the same buffer, and when both went out of scope the buffer would be freed twice. Rust prevents that at compile time by ending the first binding's claim at the point of the move. If the function only needed to read the text, it should take a str slice instead, and then nothing is moved at all. If it genuinely needs to own the value — because it stores it or returns it — then moving is correct and the caller should expect to give it up.
Intermediate
Why can a type not implement both Copy and Drop?
Model answer — Because the two make contradictory promises. Copy says that assigning the value silently duplicates its bytes and both results are valid independent values. Drop says that cleanup runs exactly once for each value when it goes out of scope. Put them together and a single assignment produces two values, each of which will have its scope end, so the destructor runs twice on what was conceptually one resource — closing a file descriptor twice, or freeing an allocation twice. That is the double-free that the ownership system exists to prevent, so the compiler rejects the combination at the definition with E0184 rather than letting it fail later. The practical consequence is a useful rule: any type that manages a resource moves and never copies, and that is precisely what guarantees its cleanup runs once.
Advanced
A codebase has clone() calls scattered through it and the team believes Rust is just verbose. How would you diagnose and address that?
Model answer — I would treat the clones as a symptom and look at signatures, because in my experience most of them are working around a function that asked for ownership it never needed — taking String where a str slice would do, or Vec where a slice would do. That forces every caller either to surrender their data or to clone, so the cost lands on code the author never sees. The fix is to push ownership requirements to the places that genuinely store, return or consume a value, and borrow everywhere else. I would want evidence rather than instinct, so I would count allocations on a representative path with a counting global allocator before and after, which turns "this feels wasteful" into a number and protects the change from being argued about. The cultural part matters as much as the technical one: the team has learned that cloning is how you satisfy the compiler, and the habit that replaces it is reading the error bottom-up, where the suggested fix names the function rather than the call site. I would also be honest that some clones are correct and that removing them all is not the goal.
Company lens — how the same topic gets asked
micro1Practical and verbal: explain to a JavaScript developer why assigning a variable can make the original unusable. They are listening for a clear mental model, not terminology.
GoogleType-system reasoning. Expect the Copy-and-Drop contradiction, and follow-ups on what else the presence of a destructor changes about a type.
MetaAPI design: spotting a signature that takes ownership unnecessarily in a code sample, and saying what it costs every caller.
AmazonResource framing. Why a socket or file handle cannot be Copy, and what guarantees cleanup runs exactly once including when a thread panics.
Hands-on

Work the TODOs in order and predict before compiling each time. TODO 4 is the one that teaches most: the two commented lines do not produce the same error, and working out why before you compile is the real exercise — one of them is about a value that moved wholesale, the other about a field. TODO 5 is the design question underneath the whole chapter. There are at least three ways to keep the route usable while still getting its name out: borrow instead of consuming, clone the one field you need, or have consume give the route back alongside the name. Implement two of them, then say which you would defend in review and why — the answer depends on what the caller does next, which is exactly the judgement the error was asking you to make.

#[derive(Debug, Clone, Copy)]
struct Meters(f64);

#[derive(Debug)]
struct Route { name: String, legs: Vec<Meters> }

fn total(legs: &[Meters]) -> Meters {
    Meters(legs.iter().map(|m| m.0).sum())
}

fn consume(r: Route) -> String { r.name }

fn main() {
    let route = Route {
        name: String::from("lagos-ibadan"),
        legs: vec![Meters(1200.0), Meters(800.5)],
    };

    // TODO 1: does this move `legs` or borrow it? Why does `route` survive?
    let t = total(&route.legs);
    println!("A  total {:?}", t);

    // TODO 2: Meters is Copy. Predict whether `first` and route.legs[0] both work.
    let first = route.legs[0];
    println!("B  first {:?} original {:?}", first, route.legs[0]);

    // TODO 3: this takes Route by value. Predict what is still usable after it.
    let name = consume(route);
    println!("C  name {name}");

    // TODO 4: uncomment ONE line at a time. Predict the error code for each
    //         before you compile. They are not all the same error.
    // println!("{:?}", route);
    // println!("{:?}", route.legs);

    // TODO 5: make the C line work AND keep `route` usable afterwards.
    //         There are at least three ways. Find two.
    println!("D  t doubled {:?}", Meters(t.0 * 2.0));
}
A  total Meters(2000.5)
B  first Meters(1200.0) original Meters(1200.0)
C  name lagos-ibadan
D  t doubled Meters(4001.0)

Summary

  • Every value has exactly one owning binding, it is dropped when that binding goes out of scope, and assigning or passing it transfers ownership and ends the original claim.
  • The whole analysis happens at compile time — no flag on the value, nothing to inspect at run time — which is why it costs nothing and also why the compiler must sometimes refuse a program that would have worked.
  • E0382 names the cause, not just the symptom: it reports where the value came from, why a move happened, where it moved, and usually suggests the real fix, which is a signature that should have borrowed.
  • Copy is a claim about validity, not about size: a type may be Copy only if duplicating its bytes yields a second valid independent value, which excludes anything owning a heap allocation.
  • Clone is the explicit, possibly expensive alternative, deliberately spelled out at the call site so an unnecessary copy is visible in review.
  • A type cannot be both Copy and Drop — E0184 — because silent duplication plus once-per-value cleanup is a double free, which is the exact bug the system exists to prevent.
  • Any type managing a resource therefore moves rather than copies, and that is what guarantees its cleanup runs exactly once, including on a panic unwind.
  • Ownership is tracked per field, not per variable, so a partial move leaves the untouched fields usable and only the struct as a whole unusable.
  • Several constructs move without saying so: for x in collection consumes it via into_iter, and a move closure captures by value and ends the original binding.
  • The habit that replaces cloning is reading the error bottom-up and fixing the signature, taking ownership only when you will store, return or consume the value.

In the next chapter, Borrowing and the One Rule, we meet the concession that makes all of this liveable: how to look at a value without taking it, the single rule governing how many references may exist at once, and why that one rule is simultaneously the thing that prevents data races and the thing you will argue with most.

Editorial reflection

It is worth noticing how little new machinery this chapter introduced. There was no runtime component, no bookkeeping structure, no clever data layout — only a rule about who is responsible for a value and a compiler that checks it. That is the unusual thing about ownership: it is not a feature in the normal sense, it is a restriction, and all of its benefits come from what it refuses to let you write. Most language features make more programs expressible. This one makes fewer, and the claim is that the programs it removes are ones you did not want. Whether you find that bargain appealing is close to the question of whether you will enjoy Rust — and it is entirely reasonable to decide, for a particular piece of software, that the programs being removed were worth more than the guarantee being bought.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.