You have written the logic. It compiles in your head. Then cargo check fires three errors about borrow checker, one about a lifetime that "does not live long enough," and a Send complaint buried inside a tokio::spawn. You stare at the wall for twenty minutes. This is the moment where an LLM either saves you two hours or quietly makes things worse — and the difference is entirely in how you prompt it.
Below are twelve prompts that turn a chat model into a Rust pair programmer. Each one has a job: not "write my code," but "explain the constraint I am fighting and show me the idiomatic escape hatch." Every prompt comes with an input and an expected output shape, so you know where the model genuinely helps and where you must verify with cargo clippy and cargo check before trusting a line.
Prompt 1 — Turning a borrow checker error into an ownership lesson
The compiler tells you where it failed, not why your mental model is wrong. This prompt forces the model to reconstruct the ownership story.
You are a Rust mentor. Here is my code and the exact compiler error.
Explain, step by step, which value owns what, where the borrow starts
and where it ends, and why the compiler rejects this. Then give the
minimal fix that keeps ownership semantics honest — do not just clone.
Code:
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);
println!("{first}");
}
Error: cannot borrow `v` as mutable because it is also borrowed as immutable More on the topic: “[Rust](/en/blog/rust-sistemnoe-programmirovanie-chto-stoit-za-kursom-i-kak-eto-rabotaet-na-praktike)”.
Expected output: a walkthrough that names first as an immutable borrow living until println!, then the fix — either move the println! before the push, or bind let first = v[0]; to copy the value. The model should explain why .clone() here is a smell, not a solution.
Where it earns its keep: beginners. Where to double-check: if the model suggests RefCell, ask it to justify the runtime cost before you accept.
Prompt 2 — Choosing between Box, Rc, Arc and the lock family
This is the decision that trips people coming from garbage-collected languages. A single prompt that lays out the trade-off table beats ten Stack Overflow tabs.
I need shared access to a value across [threads / a single thread / a tree structure].
Compare Box, Rc, Arc, RefCell, Mutex and RwLock for this case.
For each, state: ownership model, thread safety, runtime cost, and
the failure mode if I pick wrong. End with one recommendation and why.
Expected output: a compact comparison. Rc is single-threaded reference counting — cheap, but not Send. Arc adds atomic increments, so it is Send + Sync but pays for atomics on every clone. Mutex guards mutation; RwLock helps only when reads vastly outnumber writes.
| Need | Pick | You pay |
|---|---|---|
| Single owner, heap | Box |
Indirection |
| Shared, one thread | Rc + RefCell |
Runtime borrow check |
| Shared, many threads | Arc + Mutex |
Atomic + lock contention |
| Read-heavy, many threads | Arc + RwLock |
Writer starvation risk |
The trap: reaching for Arc<Mutex<T>> reflexively. If the data is read-only after setup, Arc<T> alone is enough and you skip the lock entirely. More on the topic: “From Idiomatic Go to Data Race Hunting”.
Prompt 3 — Deriving lifetimes from the compiler message
Lifetime errors are the ones people paste into a chat and hope. The trick is to make the model derive the annotation rather than guess it.
Given this function signature and the error "missing lifetime specifier,"
show me how the compiler infers lifetimes. Then propose the explicit
annotation and explain which input each output lifetime ties to.
fn longest(a: &str, b: &str) -> &str { if a.len() > b.len() { a } else { b } }
Expected output: an explanation that the return borrows from one of the inputs, so it needs fn longest<'a>(a: &'a str, b: &'a str) -> &'a str, plus a note that the elision rules cannot decide this case because there are two input references. The model should remind you that a lifetime annotation never changes how long a value lives — it only describes a relationship the compiler already enforces.
Prompt 4 — Diagnosing Send/Sync failures in async code
You spawn a task, and tokio refuses: the future is not Send. This one is worth its own prompt because the error is buried and the cause is usually a held reference across an .await.
My tokio::spawn fails with "future cannot be sent between threads safely."
Walk through the future, identify any non-Send value held across an
.await point, and show how to restructure it (drop before await, or
use a Send-safe type). Here is the code: [paste]
Expected output: the model points at the offending value — often an Rc, a RefCell, or a MutexGuard held across .await — and shows the fix: acquire the guard in a tight scope, drop it, then await. For a deeper map of async pitfalls, the breakdown in Rust Prompts That Make LLMs Write Safe Code covers the async surface in more depth.
Prompt 5 — Translating a C++ or Go idiom into Rust
If you write Go, you reach for a shared map guarded by a mutex. If you write C++, you reach for a raw pointer. Both instincts fight Rust. This prompt bridges the mental model. More on the topic: “Go and Rust”.
I would do this in Go with a sync.Map shared across goroutines.
Show the idiomatic Rust equivalent, explain what changes about
ownership and synchronization, and what I lose in ergonomics. More on the topic: “Rust for Web (Actix/WASM)”.
Expected output: an Arc<RwLock<HashMap<..>>> or, better, a channel-based design where ownership moves instead of being shared. The model should name the shift: Rust pushes you toward message passing or scoped borrowing instead of ambient shared state.
Prompt 6 — Cutting allocations without breaking correctness
Performance prompts are where models hallucinate most, so constrain them.
This hot loop clones a Vec on every iteration. Suggest allocation
reductions using borrowing, iterators, or pre-allocated capacity.
For each suggestion, state the borrow-checker implication and whether
it changes the public API. Code: [paste]
Expected output: replacing for i in 0..v.len() { process(v[i].clone()) } with an iterator over references, or hoisting Vec::with_capacity. Verify with cargo clippy -- -W clippy::perf.
Prompts 7–12 — the remaining six, in brief
The same discipline applies: give the model the error, the code, and a constraint.
- Prompt 7, trait bounds: ask it to relax an over-constrained generic to the minimum bound and explain what each bound buys.
- Prompt 8,
?and error types: convert aBox<dyn Error>mess into athiserrorenum and show theFromimpls. - Prompt 9, iterator chains: rewrite a nested loop as an iterator chain and flag where
collectforces an unnecessary allocation. - Prompt 10,
Pinand self-reference: explain why a struct cannot hold a reference to itself and whatPinactually guarantees. - Prompt 11,
dynvs generics: decide between static dispatch and trait objects for a plugin list, with the monomorphization cost named. - Prompt 12, clippy triage: paste clippy warnings and ask which are real bugs versus style, ranked by risk.
Each of these returns an explanation plus a diff. Run cargo check and cargo clippy on every diff before it touches your branch. The model is a fast explainer and a slow, unreliable compiler — use it for the first job, never the second.
The honest takeaway: an LLM will not make the borrow checker disappear, and it should not. What it does well is turn a cryptic error into a sentence you understand, so the next time the compiler says no, you already know why. More on the topic: “Go, LLM, and the Race Against Races”.