Ownership in a real Rust program: choose who keeps the data
Build a tiny message workflow to understand moves, shared borrows, mutable borrows and cloning. Learn to choose function signatures from the job they do.
2026-10-10 · 10 min read
Give ownership a concrete job
Imagine a small program that receives a message, counts its characters, appends a note, and produces a final report. The message is ordinary text stored in a String. Several functions need access to it, but they do not all need the same kind of access. Counting reads. Appending changes. Producing the final report may take responsibility for the original text. That difference is where ownership becomes useful in everyday design.
Before writing signatures, describe what each function promises its caller. Does the caller still need the message afterwards? Should the function be able to change it? Does the function need to keep it after returning? Those questions are easier to answer than a vague question about satisfying the borrow checker. We will use one message throughout this article and change one decision at a time, so you can predict the effect of each signature.
Reading a message, editing it, and taking it into a report are three different jobs.
Start with one owner
Create let message = String::from("hello");. The binding named message owns this String value. The string manages its text storage, and dropping the owned value releases that storage. Think of the owner as the place responsible for the value's lifetime, rather than a label attached permanently to one physical address. The compiler follows how responsibility moves through your program.
For this example, write a tiny ownership ledger beside the source. Before a function call, the caller owns the message. During the call, note whether ownership transfers or the function only receives a reference. After the call, mark which value remains available. The ledger is not code and does not need a sophisticated diagram. Three lines on paper can reveal why a later use is valid, or why it attempts to use a value that has already moved.
Suppose into_report accepts a String and returns a newly formatted String. Calling into_report(message) transfers the argument into the function. The caller cannot continue using the moved binding afterwards. This is often a useful API: the caller has finished working with the original message, and the function is explicitly responsible for turning it into the final representation.
The returned report is a value with a new owner in the caller. Our ledger therefore changes from caller owns message, to function owns message, to caller owns report. The exact internal implementation may allocate a new buffer, reuse storage, or combine values; the ownership contract tells us which values can be used at each point. Avoid imagining that every move means copying all the text bytes. Responsibility and copying are different questions.
Counting characters does not need to own the message. Give that function a &str: a borrowed view of text. The caller keeps ownership, and the function reads through the view for the duration of the call. character_count(&message) works because the owned string can be borrowed as a string slice. The signature also accepts a string literal, so callers are not forced to construct an owned string just to ask a read-only question.
Our reading operation counts Unicode scalar values with chars().count(). That is deliberately more specific than counting visible characters: some displayed characters consist of several scalar values. String::len() measures bytes, which answers another question. Choose the measurement your program actually needs and name it accurately. Ownership does not settle that product decision for you. A program can be memory-safe and still count the wrong thing if its contract is unclear.
Appending a note changes the caller's string, so append_note takes &mut String. The caller must have a mutable binding and must pass a mutable reference. The function can update the value without taking it away. This makes the effect visible in the signature: someone reading the call sees that this operation may change the message they still own.
While a mutable borrow is active, it provides exclusive access to the borrowed value under Rust's borrowing rules. You cannot also use an overlapping shared reference in a way that conflicts with that access. Organise the work into phases: inspect, finish inspecting, then edit. The compiler tracks when references are used; the relevant borrow can end before the surrounding block ends. Small sequential phases usually express the intended workflow more clearly than a tangle of references kept alive unnecessarily.
Now combine the three operations. Count the text while borrowing it, append the note with a mutable borrow, then move the finished message into the report. At every stage, ask who owns the current value and which access is active. The caller keeps the string during the first two jobs and deliberately gives it up for the last. That is a complete ownership story with no extra clone.
Try changing the order before changing the signatures. If you produce the report first and then attempt to count the original binding, you will encounter a moved-value error. That error reflects the contract you selected. Decide whether the programme really needs the original message later, or whether the count should simply happen earlier. A change in operation order may solve the actual design problem more cleanly than adding another allocation.
Sometimes the application really needs two independently owned strings. Perhaps you must preserve the original user input while building an editable draft. In that case, cloning the string can express a deliberate requirement. Name the values according to their roles: original and draft. A reader can then see why two owned copies exist and why later edits to the draft should not alter the original.
Cloning is type-specific. For a String, it creates an independently owned copy of the text; for other types, the behaviour and cost depend on their Clone implementation. Do not apply a universal rule that cloning is always wrong or always cheap. Instead, explain the independence you need. If a function only reads, borrowing may be enough. If two parts need separate ownership and independent changes, a clone may be a reasonable part of the design.
A function can return a borrowed part of the caller's text. Our first_word function takes a &str and returns a &str selected from it. With this signature, the lifetime elision rules connect the returned reference to the input reference. The caller must keep the underlying text valid for as long as it uses that returned view. The function does not create a new owned copy of the word.
This choice is useful when inspecting a message briefly. If you instead need to save the word independently while discarding or changing the source, an owned result may be the clearer contract. Practise both versions and write down what changes in the ledger. The borrowed version depends on source storage; an owned string has its own ownership story. Neither version is universally better. The requirement to keep or modify the source determines which contract fits.
When the borrow checker rejects a change, locate the places it identifies: where the value moved or a borrow began, and where the conflicting use occurs. Annotate those source lines with your ledger. If you cannot describe the sequence, reduce the example until you can. Keep the same relationship between owner and references while removing unrelated printing, parsing or collection code.
Then propose a change in ordinary language before editing Rust. I need the reading phase to finish before mutation. I need this function to borrow because the caller still uses the value. I need an owned result because its source will disappear. Each statement leads to a specific code change. This habit turns diagnostics into design feedback. Randomly inserting clone, mut or lifetime parameters can make the compiler quieter without making your data flow easier to understand.
Small numbers have a different contract
If you pass the character count into another function, you may notice that the count remains usable afterwards. That is because a simple integer type such as usize implements Copy. Passing that value can copy it without the explicit ownership transfer behaviour you observed for String. This does not make the message example inconsistent; different types provide different capabilities. Read the type before applying a rule learned from one particular value.
Add a helper that prints the count and then use the count again in the caller. Compare its signature with the function that consumes the message. Write two ledger entries: one for the integer copy, one for the string move. The exercise prevents a common overgeneralisation that every ordinary assignment or argument makes the previous binding unusable. Your model should describe what the selected type permits. When a type does not implement Copy, make the ownership decision explicit through moving, borrowing or a deliberate clone.
Build a message workflow in Crabrix or on your desktop. Begin with the combined example above. Add a second read-only function that reports whether the message contains a question mark. Call it before appending the note. Then preserve an original string and create a separate draft that receives the note. Explain why the second version needs independent ownership, and compare the output of original, draft and final report.
Finally, return the first word as a borrowed view and deliberately attempt a conflicting edit before using the view. Read the diagnostic, then reorganise the work so the view is consumed before mutation. Your finish condition is not merely a green build: describe every move and borrow in the successful programme, identify the storage behind each view, and explain any clone you retained. Once that explanation is straightforward, ownership has become a tool you can use rather than a rule you are trying to evade.