Rust is famous for its steep learning curve, and the borrow checker is often the first wall developers hit. LLMs can help you scale that wall faster, but only if you ask the right questions. A vague prompt like "write a Rust function" will get you code that compiles but fights the compiler, leaks lifetimes, or silently blocks an async runtime. This roundup gives you 10 concrete prompts, each with a real example, to turn an LLM into a Rust pair programmer that respects ownership, lifetimes, and async semantics.
Each prompt follows a task → prompt → example result structure. Use them as-is or adapt them to your codebase. The goal is not to avoid learning Rust, but to make the LLM surface the exact compiler reasoning you need to internalize.
1. Diagnose a borrow checker error and propose fixes
Task: When the compiler rejects your code with E0502 or E0499, you need to understand why and get a minimal fix, not a workaround that clones everything.
Prompt:
You are a Rust compiler expert. I have this error:
[PASTE ERROR]
Here is the minimal code that reproduces it:
[PASTE CODE]
Explain in plain English why the borrow checker rejects this. Then propose two fixes: one that changes ownership structure, one that changes lifetimes. Do not suggest .clone() unless it is the only idiomatic option. Show the fixed code and explain the trade-off.
Example result: For a classic error where you try to mutate a Vec while iterating over it, the LLM explains that the iterator holds an immutable borrow of the vector, so mutation is forbidden. It suggests collecting indices first or using retain. It shows the fixed code with retain and notes that cloning the vector would be O(n) extra memory and hide the real design issue.
2. Translate a mutable algorithm into ownership-friendly Rust
Task: You have a Python or C++ algorithm that mutates data in place, and you want a Rust version that does not fight the borrow checker.
Prompt:
Here is a Python function that mutates a list while iterating:
[PASTE CODE]
Rewrite it in Rust. Prefer owned data and iterators over manual indices. If mutation during iteration is required, show the idiomatic Rust pattern (e.g., retain, drain, or split_at_mut). Explain why your version compiles without unsafe.
Example result: The LLM rewrites a Python loop that removes items from a list into Rust using Vec::retain, explaining that retain takes a closure with a shared borrow and handles removal internally, so the borrow checker is satisfied. It also shows an alternative with drain if you need to process removed items.
3. Generate lifetime annotations with a rationale
Task: You have a struct or function with references, and the compiler asks for explicit lifetimes. You want the LLM to write them and explain the elision rules it applied.
Prompt:
Given this code:
[PASTE CODE]
Add explicit lifetime annotations. For each lifetime, state which reference it ties together and why elision does not apply. Then show the code after annotation. If a lifetime can be avoided by changing ownership, propose that alternative and compare the trade-offs.
Example result: The LLM adds <'a> to a struct holding a string slice and explains that the struct cannot outlive the borrowed data. It also suggests storing a String instead if the struct needs to be 'static, noting the cost of allocation.
4. Convert blocking code to async without deadlocks
Task: You have synchronous I/O or CPU-bound code and want to move it into an async context without blocking the executor.
Prompt:
I have this blocking function:
[PASTE CODE]
Show how to call it from an async function using tokio. If it is CPU-bound, use spawn_blocking; if it is I/O-bound, use the appropriate async API. Explain why calling it directly inside async would block the runtime. Include error handling.
Example result: For a function that reads a large file synchronously, the LLM wraps it in tokio::task::spawn_blocking and awaits the handle. It explains that blocking the executor thread prevents other tasks from progressing. It also shows the ? operator for error propagation and notes that spawn_blocking requires the closure to be Send + 'static.
5. Design a Send + Sync safe shared state
Task: You need to share mutable state across async tasks or threads without data races.
Prompt:
I need shared mutable state of type [TYPE] across [threads/async tasks]. Propose the simplest safe solution using std::sync or tokio::sync. Explain why your choice satisfies Send + Sync. Show initialization, read, and write examples. Mention any performance implications (lock contention, async vs sync mutex).
Example result: For a counter shared across async tasks, the LLM suggests Arc<tokio::sync::Mutex<u64>> and explains that tokio::sync::Mutex is async-aware and does not block the executor, unlike std::sync::Mutex. It shows Arc::clone for each task and warns about holding the lock across .await points.
6. Refactor a function to return Result instead of panicking
Task: You have code full of unwrap() and expect(), and you want to propagate errors properly.
Prompt:
Refactor this function to return Result<_, E> instead of panicking:
[PASTE CODE]
Define a custom error type using thiserror. Show the refactored code, the error enum, and how callers should handle each variant. Do not use anyhow in the library code; use it only in main or tests.
Example result: The LLM replaces unwrap() with ? and defines an enum with variants for I/O and parse errors. It shows a main that uses anyhow::Result and prints a user-friendly message.
7. Write property-based tests for unsafe code
Task: You have a small unsafe block and want to validate its invariants with tests.
Prompt:
Here is an unsafe function:
[PASTE CODE]
Write property-based tests using proptest that check memory safety invariants: no out-of-bounds, no aliasing violations, and correct behavior on edge cases. Explain what each property verifies and why it matters.
Example result: For a function that uses get_unchecked, the LLM writes a proptest that generates random indices and asserts they are within bounds before calling, and a separate test that catches panics. It explains that property tests complement, not replace, careful reasoning about unsafe.
8. Explain a complex trait bound error
Task: Trait bound errors in async code can be cryptic. You want a plain-English breakdown.
Prompt:
I get this error:
[PASTE ERROR]
The code is:
[PASTE CODE]
Explain what trait bound is missing, why the compiler requires it, and how to fix it without removing async. If the fix involves adding Send bounds, explain which type is not Send and why.
Example result: The LLM identifies that a Rc is held across an .await, making the future non-Send. It suggests replacing Rc with Arc and explains that Rc is not thread-safe, so the future cannot be sent between threads.
9. Generate a safe FFI wrapper
Task: You need to call a C library from Rust and want a safe abstraction.
Prompt:
I need to wrap this C function:
[C HEADER]
Write a safe Rust wrapper using bindgen-style declarations. Encapsulate all unsafe code, validate inputs, and return Result. Show the wrapper and a usage example. Explain which invariants the wrapper enforces.
Example result: The LLM creates a struct that owns the raw pointer and implements Drop to free it. It checks for null pointers and returns an error. It explains that the wrapper prevents use-after-free by tying the lifetime to the struct.
10. Review async code for common pitfalls
Task: Before merging, you want a checklist review of async code.
Prompt:
Review this async Rust code for common pitfalls: blocking calls inside async, holding locks across await, unbounded concurrency, missing timeouts, and error swallowing. For each issue, show the problematic line and a fix. Be specific.
[PASTE CODE]
Example result: The LLM flags a std::fs::read inside an async function, suggests tokio::fs::read, and points out a MutexGuard held across .await. It also suggests adding tokio::time::timeout around a network call.
11. Convert a callback-based API to async/await
Task: You have an API that uses callbacks or channels and want to expose it as an async function.
Prompt:
Convert this callback-based API to an async function using tokio::sync::oneshot or mpsc:
[PASTE CODE]
Show the wrapper, explain how the channel coordinates the callback and the future, and handle cancellation.
Example result: The LLM wraps the callback in a oneshot channel, spawns the callback registration, and awaits the receiver. It explains that if the future is dropped, the sender will fail to send, which is handled gracefully.
12. Generate documentation with safety invariants
Task: You want /// docs that explain safety requirements for unsafe functions.
Prompt:
Write Rustdoc comments for this unsafe function:
[PASTE CODE]
Include a `# Safety` section listing all invariants the caller must uphold. Explain each invariant in one sentence. Show a doc example with `should_panic` if misuse is possible.
Example result: The LLM adds a # Safety section stating that the pointer must be valid and aligned, and that the caller must ensure no aliasing. It includes a doc test that demonstrates correct usage.
These prompts are not a substitute for learning Rust, but they turn the LLM into a patient mentor that explains the why behind compiler errors. The key is to demand explanations and trade-offs, not just code. Use them in your daily workflow, and you will find that the borrow checker becomes a guide rather than a barrier.
For more AI-assisted development techniques, see our React prompts guide and web security practices.