Skip to content
Select theme

Why can't you use a value after moving it?

Short answer: a move leaves exactly one binding owning the value, and ownership is what decides where the value gets freed. So the old name isn’t stale, it’s unowned — using it again would claim ownership a second time. The compiler rejects that claim while compiling, which is why nothing has to check for it while the program runs.

Ownership is the compiler’s record of who is responsible for cleaning up. Give the value a destructor that prints and the record becomes something you can count:

moved holds "hello"
freeing "hello"

One buffer, one freeing line, and it lands after the last statement in main — at the point moved goes out of scope. The move neither added a free nor removed one; it handed the single free to the new binding. That half of the story has its own page, what happens to the original variable after a move.

Two freeing lines aren’t the bug. Two names for one buffer would be. Ask for a genuine second buffer and you get two of each, which is correct:

original: "hello"
copy: "hello"
freeing "hello"
freeing "hello"

clone allocates, so both names own a buffer of their own and both frees are earned. What the compiler guards against is the other combination — one buffer with two owners — and a plain second use is how you would ask for it by accident.

error[E0382]: use of moved value: `message`
--> src/main.rs:5:18
|
2 | let message = String::from("hello");
| ------- move occurs because `message` has type `String`, which does not implement the `Copy` trait
3 | let first = message;
| ------- value moved here
4 |
5 | let second = message;
| ^^^^^^^ value used here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let first = message.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.

The second let isn’t reading message, it’s asking to take it — which is why the heading here is use of moved value rather than the more familiar borrow of moved value. The caret sits on the second claim, not the first: the assignment on line 3 is legal, and doing it twice is what fails.

Let it through and both names would own the one buffer, so both would free it at the end of main: two freeing lines for a single allocation. That’s the double free, and refusing the second let is how it gets prevented.

Only one of those two programs got as far as running. The second was rejected while compiling, so it never produced a binary and there’s no later run in which the mistake can surface. What the compiler settled up front is which binding owns the value, and therefore where the one drop call in the compiled program goes. Both halves are visible above: the version that compiled printed freeing once, and the version that didn’t compile never ran at all.

Genuinely shared ownership is still possible, but it needs a type that counts owners while the program runs — a cost you opt into by naming such a type. Smart pointers and sharing is where those live.