Rust take-homes are a different kind of challenge. The interviewer isn't just running your test suite — they're reading your code like a teammate would in a code review. Every .clone(), every .unwrap(), every index-based loop tells them something about how you think in Rust.
These five mistakes show up in the majority of bootcamp grad submissions we've reviewed. Fix them and your code will read like it was written by someone who actually works in Rust every day.
1. Fighting the Borrow Checker Instead of Working With It
The single biggest red flag in bootcamp grad submissions is a trail of .clone() calls scattered through the code. Every unnecessary clone signals the same thing: you hit a borrow error, panicked, and added .clone() until the compiler stopped complaining. Reviewers see this immediately.
The fix is to slow down when you hit a borrow error and ask one question: does this function need to own the data, or just read it? Most of the time, you want a reference.
// Bad — cloning to silence the compiler
fn word_count(text: String) -> usize {
text.clone().split_whitespace().count()
}
let doc = String::from("hello world");
word_count(doc.clone()); // now doc is gone AND you cloned inside
// Good — borrow a slice, keep ownership where it belongs
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
let doc = String::from("hello world");
word_count(&doc); // doc still lives, no allocation
Rule of thumb: pass &str instead of String, &[T] instead of Vec<T> for any function that only reads data. Reserve owned types for constructors and functions that store the data.
2. Using .unwrap() Everywhere
.unwrap() is a panic waiting to happen. In a take-home, every .unwrap()tells the reviewer: "I know this could fail and I chose to crash the program instead of handle it." One or two with a comment explaining why failure is impossible is fine. Fifteen of them throughout your code is not.
// Bad — a panic bomb in production code
fn parse_config(path: &str) -> Config {
let contents = fs::read_to_string(path).unwrap();
serde_json::from_str(&contents).unwrap()
}
// Good — propagate errors, let the caller decide
fn parse_config(path: &str) -> anyhow::Result<Config> {
let contents = fs::read_to_string(path)?;
Ok(serde_json::from_str(&contents)?)
}
Use anyhow for application code and thiserror for library code. The ? operator propagates errors cleanly and is idiomatic Rust. If you catch yourself writing .unwrap(), ask whether you can use ? instead.
Stuck on your take-home right now?
Don't spend another hour fighting the same error. Get a live 1-on-1 session with a senior Rust engineer who's reviewed hundreds of take-home submissions. We work through your specific problem together.
3. Adding Lifetime Annotations Where They're Not Needed
After learning about lifetimes, many bootcamp grads go one of two ways: they either scatter 'a annotations everywhere to show they know about them, or they avoid references entirely and clone everything (see mistake #1). Both fail the review.
The rule is simple: add explicit lifetimes only when the compiler requires them and you can't reorganize ownership to avoid them. Rust's lifetime elision rules handle the common cases automatically.
// Bad — explicit lifetimes the compiler would infer anyway
fn first_word<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
// Good — elision handles it; add lifetimes only when needed
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
Explicit lifetimes are necessary when a function returns a reference derived from one of multiple input references, or when a struct holds a reference. In those cases, the compiler will tell you. Don't add them pre-emptively — it reads like noise and suggests you don't understand when they're actually required.
4. Writing Python or JavaScript in Rust (Non-Idiomatic Loops)
Index-based loops are the fastest way to tell a reviewer you're translating from another language. Rust has a rich iterator API — use it. Code that reaches for for i in 0..vec.len() instead of .iter()and combinators reads like a direct port from Python, and that's exactly how interviewers describe it.
// Bad — imperative, index-based, non-idiomatic
let numbers = vec![1, 2, 3, 4, 5];
let mut evens = vec![];
for i in 0..numbers.len() {
if numbers[i] % 2 == 0 {
evens.push(numbers[i]);
}
}
// Good — idiomatic iterator pipeline
let numbers = vec![1, 2, 3, 4, 5];
let evens: Vec<_> = numbers.iter()
.filter(|&&n| n % 2 == 0)
.copied()
.collect();
Run cargo clippy before submitting — it will flag non-idiomatic loops and suggest the iterator equivalent. Other common tells: manual if/else chains on Option instead of .map()/.unwrap_or(), and building strings with push_str in a loop instead of .collect().
5. Not Testing Edge Cases (Panics on Empty Input)
Reviewers always test your code with empty input, single-element collections, and boundary values. If your code panics on an empty slice or returns a wrong answer on a single-character string, that's an immediate red flag — not because the edge case is hard, but because it shows you didn't think about it at all.
// Bad — panics on empty input
fn max_value(nums: &[i32]) -> i32 {
let mut max = nums[0]; // panics on empty slice
for &n in &nums[1..] {
if n > max { max = n; }
}
max
}
// Good — returns Option, forces callers to handle the empty case
fn max_value(nums: &[i32]) -> Option<i32> {
nums.iter().copied().max()
}
// In tests:
assert_eq!(max_value(&[]), None);
assert_eq!(max_value(&[5]), Some(5));
assert_eq!(max_value(&[3, 1, 4, 1, 5]), Some(5));
Before submitting, manually run through these cases for every function: empty input, single element, all-same elements, and the largest/smallest valid input. Write tests for them. A test file with only the happy path is a missing signal that you don't think defensively.
Free Resource
The Rust Take-Home Survival Guide
A free reference covering the seven patterns that come up most in Rust take-homes: borrow checker mental models, lifetime explanations, error handling with Result, idiomatic iterators and traits, reading compiler errors by code, debugging under deadline pressure, and when live help pays off.
The Quick Checklist Before You Submit
Run through this before you hit send on any Rust take-home:
- No gratuitous .clone() calls: If you're cloning to satisfy the borrow checker, try passing a reference instead.
- No .unwrap() outside of tests: Every failure path should return a Result or Option, not panic.
- No unnecessary lifetime annotations: Let elision do its job. Add 'a only when the compiler requires it.
- cargo clippy passes clean: Clippy will surface non-idiomatic loops, redundant clones, and more.
- Tests cover empty and boundary inputs: Empty slices, empty strings, single-element collections, min/max values.