From Borrow Checker to Production: Rust Prompts That Turn the Compiler Into Your Mentor
Rust keeps winning developer surveys for "most loved language" — it has topped Stack Overflow's annual survey for years running — yet the same survey consistently shows it near the top of "most wanted to learn" while adoption lags behind. The gap isn't hype. It's the borrow checker. Ownership, lifetimes, and the Send/Sync dance are where most newcomers stall, and where experienced developers coming from Go, Python, or Node.js suddenly feel like beginners again.
The good news: Rust's compiler is arguably the best teacher in the entire ecosystem. Its error messages are famously detailed, often including suggested fixes. The trick is learning how to interrogate it — and how to use an AI assistant as a patient pair programmer that explains why the borrow checker rejects your code, not just how to silence it.
Below is a working collection of prompts I've used and refined while building backend services in Rust. They're grouped so you can move from "what is this error" to "how do I structure a Tokio service that doesn't deadlock under load." Each one is a real scenario, a copy-pasteable prompt, and what you should expect back. Treat them as scaffolding — the goal is understanding, not cargo-culting.
1. Decode a Borrow Checker Error Like a Human Would
Task: Stop guessing at E0502/E0499 and get a plain-language explanation plus a fix.
Prompt:
I'm learning Rust. Here's my code and the compiler error:
[paste code]
[paste full `cargo build` output]
Explain in plain language:
1. What ownership rule I violated (name the specific error code).
2. Why the compiler can't prove my code is safe here.
3. Two possible fixes — one that changes the data structure, one that changes the borrow pattern.
4. Which fix is idiomatic Rust and why.
Don't just give me the fixed code; explain the reasoning.
Example result: For a classic "cannot borrow v as mutable because it is also borrowed as immutable" (E0502), the model typically explains that a &v[0] reference is still alive when you call v.push(...), then offers either cloning the element, restructuring to compute the value before mutating, or using indices instead of references. The value is the why — you start recognizing the pattern instead of memorizing the error number.
2. Turn a Lifetime Puzzle Into a Diagram
Task: Lifetimes ('a) are the second wall after ownership. Visualize them.
Prompt:
Here's a function with explicit lifetimes I don't understand:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }
Draw me an ASCII timeline showing:
- when each reference is created,
- how long each must stay valid,
- where the returned reference points.
Then rewrite the signature without explicit lifetimes and explain when Rust infers them for me (the elision rules).
Example result: You get a timeline where the returned &str is bound to the shorter of the two input lifetimes — which is the whole point of the 'a annotation. This prompt is the single fastest way I've found to make elision rules click: seeing that fn first_word(s: &str) -> &str works because of the one-input-lifetime rule.
3. Convert a Python/JS Function to Idiomatic Rust
Task: Translate mental models, not just syntax.
Prompt:
Translate this Python function to idiomatic Rust:
[paste function]
Requirements:
- Prefer iterators over manual loops.
- Avoid unnecessary .clone() — explain each clone you do keep.
- Return Result<T, E> instead of raising exceptions; suggest a real error type.
- Add a #[test] module with 3 cases including an edge case.
Then explain the 3 biggest mental-model shifts I should internalize coming from Python.
Example result: A list comprehension becomes .iter().filter(...).map(...).collect(). The explanation usually highlights that Rust pushes error handling into the type system (Result), that mutation is explicit (let mut), and that the compiler enforces the invariants Python leaves to runtime. This is where switching languages stops feeling like translation and starts feeling like learning.
4. Design a Type-Safe Domain Model Before Writing Logic
Task: Use the type system to make illegal states unrepresentable.
Prompt:
I'm modeling an order system. States: Draft, Submitted, Paid, Shipped, Cancelled.
Rules: you can't ship an unpaid order; you can't cancel a shipped order.
Design Rust types so these rules are enforced at compile time, not with runtime checks.
Show the enum/struct layout, the methods that transition states, and explain how the compiler
prevents me from calling .ship() on a Draft. Include a small example of code that FAILS to compile.
Example result: Typically a state-machine enum with per-state structs, or a typestate pattern where Order<Draft> has a submit(self) -> Order<Submitted> method. The killer detail is the "code that fails to compile" — seeing error[E0599]: no method named ship found for Order<Draft> is more convincing than any blog post about type safety.
5. Build a Minimal Tokio Service, Step by Step
Task: Get from "hello world" to a real async HTTP handler without drowning in boilerplate.
Prompt:
Write a minimal async HTTP service in Rust using Tokio and Axum.
Requirements:
- One GET route that reads a value from a shared in-memory state.
- Shared state via Arc<Mutex<...>> — and explain why we need Arc and Mutex here.
- Show the Cargo.toml with exact crate versions.
- Explain the difference between tokio::spawn and .await in one paragraph.
- Point out the most common beginner mistake in this exact setup.
Example result: A runnable main.rs with #[tokio::main], an Arc<Mutex<HashMap<...>>> state, and a handler. The "common mistake" is usually using a blocking std::sync::Mutex across .await points, which can deadlock — a real footgun documented in Tokio's own guidance. This prompt teaches the async runtime's rules through a concrete service.
6. Diagnose "Future Is Not Send" Errors
Task: The Send bound error is the async equivalent of the borrow checker's bite.
Prompt:
I'm getting this error in my Tokio app:
`future cannot be sent between threads safely` ... `within this impl Future, the trait Send is not implemented`
Here's the code: [paste]
1. Explain what Send means and why Tokio's multi-threaded runtime requires it.
2. Find the exact line holding a non-Send value across an .await.
3. Give two fixes: one using a scoped block, one restructuring the code.
4. Explain how to avoid this class of bug in the future.
Example result: The culprit is almost always a Rc, a RefCell, or a guard (like a MutexGuard) held across .await. The fix is dropping it before the await. Understanding why Rc isn't Send but Arc is ties directly back to ownership — everything connects.
7. Write Property-Based Tests With proptest
Task: Move beyond example-based tests to invariants.
Prompt:
I have this function: [paste]
Write property-based tests using the `proptest` crate.
Identify 3 invariants this function should satisfy (e.g., round-trip, idempotence, monotonicity).
Show the proptest setup, the strategies for generating inputs, and how a failure prints a minimal counterexample.
Explain when property testing finds bugs that unit tests miss.
Example result: For a parser or serializer, the round-trip property (parse(serialize(x)) == x) catches edge cases humans never think to write. proptest automatically shrinks failures to the smallest input, which is far more useful than a random 10,000-character string.
8. Refactor Nested Match Statements Into Combinators
Task: Rust code can get pyramid-shaped fast. Flatten it.
Prompt:
Refactor this function to reduce nesting:
[paste function with nested match/if let]
Goals:
- Use `?` for error propagation.
- Replace nested matches with `and_then`/`map`/`ok_or` where it improves readability.
- Keep it readable — don't over-chain into unreadable one-liners.
Explain each change and when NOT to use combinators (readability trade-offs).
Example result: A 40-line nested function often becomes 12 lines. The important part is the caveat: combinators are great up to a point, after which match is clearer. The model should tell you where that line is, not just maximize cleverness.
9. Explain a Crate's API From Its Docs
Task: Onboard onto an unfamiliar crate without reading 40 pages.
Prompt:
I want to use the `[crate name]` crate for [task]. Based on its official docs.rs page,
summarize:
1. The 3 core types/traits I'll actually use.
2. A minimal working example for my task.
3. The most common pitfalls mentioned in the docs or issue tracker.
4. Whether there's a simpler alternative crate for my use case.
Cite the doc sections you're drawing from.
Example result: For something like serde, you get Serialize, Deserialize, and the derive macros, plus the pitfall about #[serde(rename)] mismatches. Always verify against docs.rs — the model can drift on version-specific APIs.
10. Profile and Optimize a Hot Path
Task: Rust is fast by default, but not magically.
Prompt:
This function is my bottleneck: [paste]
1. Identify likely performance issues (allocations, clones, bounds checks, unnecessary collect()).
2. Suggest optimizations ranked by impact vs. readability cost.
3. Show how to benchmark it with `criterion`.
4. Tell me which optimizations are premature and should be skipped.
Example result: The usual wins are removing .clone() in loops, using &str instead of String in hot paths, and avoiding .collect() when you can iterate lazily. The "premature optimizations" section is the most valuable — it keeps you from trading clarity for gains you'll never measure.
11. Review My Module Structure Like a Senior Engineer
Task: Architecture feedback before the codebase calcifies.
Prompt:
Here's my project layout:
[tree output of src/]
Act as a senior Rust engineer doing a code review. Assess:
1. Module boundaries — is anything doing too much?
2. Where should traits be used for abstraction vs. concrete types?
3. Error handling strategy — am I mixing anyhow and thiserror appropriately?
4. Visibility (pub) — am I exposing more than I should?
Give specific, actionable feedback, not generic advice.
Example result: Typical feedback: keep anyhow at the application boundary and thiserror for library errors; don't over-abstract with traits until you have two implementations; tighten pub to pub(crate) where possible. This is the kind of review that costs a senior engineer an hour — and it's worth practicing before you have one.
12. Explain Unsafe Code and Prove It's Sound
Task: unsafe isn't a badge of honor — it's a contract you must justify.
Prompt:
Explain this unsafe block line by line:
[paste unsafe code]
1. What invariant does the compiler stop checking here?
2. Why is each unsafe operation sound in this context?
3. What could go wrong if a caller violates the documented precondition?
4. Is there a safe alternative using std or a well-known crate?
Example result: The model should articulate the specific safety contract (e.g., "the pointer is valid because it comes from a Vec we haven't reallocated") and often points to a safe alternative. If it can't justify soundness, that's your signal to reconsider the design.
Rust rewards patience in a way few languages do: once ownership and lifetimes click, the compiler stops being an adversary and becomes a proof assistant that catches bugs your tests never would. These prompts aren't a shortcut around that learning curve — they're a way to make the curve visible. Use them to interrogate the compiler, translate your existing instincts, and design services on Tokio that hold up under load.
The pattern underneath all twelve is the same: ask why, demand the reasoning, and verify against official sources — the Rust Book, the standard library docs, and Tokio's own guides. An AI assistant that explains the borrow checker is useful; one that teaches you to reason about ownership is transformative. Start with prompt #1 on your very next error, and let the compiler become your mentor.
Comments