Toolmingo
Guides26 min read

50 Rust Interview Questions (With Answers)

Top Rust interview questions with clear answers and code examples — covering ownership, borrowing, lifetimes, traits, error handling, concurrency, async, and Rust idioms.

Rust interviews test your understanding of ownership, the borrow checker, lifetimes, traits, error handling, and safe concurrency. This guide covers the 50 most common questions — with clear answers and runnable code examples.

Quick reference

Topic Most asked questions
Ownership Move semantics, Copy vs Clone, borrowing
Lifetimes Annotations, lifetime elision, 'static
Traits Trait objects, generics, impl vs dyn
Error handling Result, ?, panic vs recoverable
Concurrency Send/Sync, Arc/Mutex, channels
Async async/await, Futures, Tokio
Memory Stack vs heap, Box/Rc/Arc, no GC
Types Structs, enums, pattern matching

Ownership & Borrowing

1. What is Rust's ownership system and why does it exist?

Rust's ownership system is a set of compile-time rules that guarantee memory safety without a garbage collector:

Rule Meaning
Each value has one owner No shared mutable state by default
Owner goes out of scope → value dropped Automatic, deterministic memory cleanup
Transfer ownership with assignment Move semantics prevent use-after-move
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;          // s1 is MOVED to s2
    // println!("{}", s1); // ❌ compile error: use of moved value
    println!("{}", s2);   // ✅
}

Ownership eliminates dangling pointers, double-frees, and data races at compile time — zero runtime cost.

2. What is the difference between Copy and Clone?

Trait When used Cost
Copy Implicit bitwise copy on assignment Zero (stack only)
Clone Explicit .clone() call May allocate heap memory

Types with Copy: integers, floats, booleans, char, tuples of Copy types.
Types without Copy: String, Vec, Box — they own heap data.

let x: i32 = 5;
let y = x;           // copied, x still valid
println!("{} {}", x, y);

let s = String::from("hi");
let t = s.clone();   // explicit deep copy
println!("{} {}", s, t);

3. What are Rust's borrowing rules?

The borrow checker enforces two rules at all times:

  1. One mutable reference OR any number of immutable references — never both simultaneously.
  2. References must always be valid (no dangling references).
let mut v = vec![1, 2, 3];

// Multiple immutable borrows — OK
let a = &v;
let b = &v;
println!("{:?} {:?}", a, b);

// Mutable borrow — OK after immutable borrows end
let c = &mut v;
c.push(4);
// ❌ Compile error: can't have mut + immut at same time
let r1 = &v;
let r2 = &mut v;  // error
println!("{}", r1);

4. What is the difference between &T, &mut T, and owned T?

Type Name Allows mutation Transfers ownership
T Owned Yes Yes (move)
&T Shared reference No No
&mut T Mutable reference Yes No
fn print_len(s: &String) {           // borrows, doesn't take ownership
    println!("{}", s.len());
}

fn make_uppercase(s: &mut String) {  // mutably borrows
    s.make_ascii_uppercase();
}

fn consume(s: String) {              // takes ownership, s dropped on return
    println!("{}", s);
}

5. What is a dangling reference and how does Rust prevent it?

A dangling reference points to memory that has been freed. Rust's borrow checker ensures references cannot outlive the data they point to:

// ❌ This will NOT compile
fn dangle() -> &String {
    let s = String::from("hello"); // s is created
    &s                             // return reference to s...
}                                  // s is dropped here! reference would dangle

// ✅ Return owned value instead
fn no_dangle() -> String {
    String::from("hello")          // ownership moved to caller
}

Lifetimes

6. What are lifetimes in Rust?

Lifetimes are compile-time annotations that tell the borrow checker how long references are valid. They prevent dangling references without runtime overhead.

// 'a is a lifetime parameter — both input refs and output ref live at least 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str());
        println!("{}", result); // ✅ both alive here
    }
    // println!("{}", result); // ❌ s2 dropped, result would dangle
}

7. What is lifetime elision?

Lifetime elision is a set of rules that allow you to omit lifetime annotations in common patterns. The compiler infers them:

// These are equivalent:
fn first_word(s: &str) -> &str { ... }
fn first_word<'a>(s: &'a str) -> &'a str { ... }

Elision rules:

  1. Each input reference gets its own lifetime.
  2. If there is exactly one input lifetime, it applies to all outputs.
  3. If one input is &self or &mut self, its lifetime applies to outputs.

8. What is 'static lifetime?

'static means the reference is valid for the entire program duration.

// String literals are 'static — stored in binary's read-only data
let s: &'static str = "hello world";

// Trait objects sometimes require 'static
fn get_formatter() -> Box<dyn std::fmt::Display + 'static> {
    Box::new(42)
}

Common use: T: 'static means T contains no non-static references (often required for threads).

9. How do you add lifetime annotations to structs?

When a struct holds a reference, it needs a lifetime parameter:

struct Excerpt<'a> {
    part: &'a str,   // struct can't outlive the string it borrows from
}

impl<'a> Excerpt<'a> {
    fn level(&self) -> i32 { 3 }

    fn announce(&self, announcement: &str) -> &str {
        println!("Attention: {}", announcement);
        self.part  // 'a elided — same as &'a str
    }
}

Traits

10. What are traits in Rust?

Traits define shared behaviour — similar to interfaces in other languages but more powerful. They can have default implementations and be used as bounds.

trait Greet {
    fn name(&self) -> &str;

    // Default implementation
    fn hello(&self) {
        println!("Hello, {}!", self.name());
    }
}

struct Person { name: String }

impl Greet for Person {
    fn name(&self) -> &str { &self.name }
    // hello() inherited for free
}

let p = Person { name: "Alice".into() };
p.hello(); // "Hello, Alice!"

11. What is the difference between impl Trait and dyn Trait?

Feature impl Trait dyn Trait
Dispatch Static (monomorphisation) Dynamic (vtable)
Performance Zero-cost, inlined Small overhead
Flexibility Single concrete type Any type at runtime
Return pos Can only return one concrete type Multiple types possible
Allocation Stack (usually) Heap via Box<dyn Trait>
// impl Trait — compiler generates specialised code per type
fn notify(item: &impl Summary) { println!("{}", item.summarize()); }

// dyn Trait — runtime dispatch via vtable
fn notify_dyn(item: &dyn Summary) { println!("{}", item.summarize()); }

// Returning impl Trait (single concrete type)
fn make_summarizer() -> impl Summary { Article { ... } }

// Returning dyn Trait (multiple possible types)
fn make_summarizer_dyn(flag: bool) -> Box<dyn Summary> {
    if flag { Box::new(Article { ... }) } else { Box::new(Tweet { ... }) }
}

12. What are trait bounds?

Trait bounds constrain generic types to only those that implement a given trait:

// Syntax 1: inline bound
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list { if item > largest { largest = item; } }
    largest
}

// Syntax 2: where clause (cleaner with multiple bounds)
fn print_info<T>(item: T) where T: Display + Debug + Clone {
    println!("{:?}", item);
}

// Multiple bounds with +
fn notify(item: &(impl Summary + Display)) { ... }

13. What are common standard library traits?

Trait Purpose
Display {} formatting
Debug {:?} formatting
Clone Explicit deep copy
Copy Implicit bitwise copy
PartialEq / Eq == operator
PartialOrd / Ord <, > operators
Iterator .next() — enables for loops
From / Into Type conversions
Default Zero/empty value
Drop Custom destructor
Send Safe to transfer across threads
Sync Safe to share reference across threads

14. What is the Iterator trait?

Any type implementing Iterator with a next() method gets all iterator adapters for free:

struct Counter { count: u32 }

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        self.count += 1;
        if self.count <= 5 { Some(self.count) } else { None }
    }
}

let sum: u32 = Counter::new()
    .zip(Counter::new().skip(1))
    .map(|(a, b)| a * b)
    .filter(|x| x % 3 == 0)
    .sum();

Enums & Pattern Matching

15. How do Rust enums differ from enums in other languages?

Rust enums are algebraic data types — each variant can hold different data:

enum Shape {
    Circle(f64),                         // tuple variant
    Rectangle { width: f64, height: f64 }, // struct variant
    Triangle(f64, f64, f64),             // tuple variant with 3 fields
    Point,                               // unit variant (no data)
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r)           => std::f64::consts::PI * r * r,
        Shape::Rectangle { width, height } => width * height,
        Shape::Triangle(a, b, c)  => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
        Shape::Point => 0.0,
    }
}

16. What is Option<T> and why does Rust use it instead of null?

Option<T> forces you to handle the absence of a value — null pointer exceptions are impossible:

enum Option<T> { Some(T), None }

fn find_user(id: u32) -> Option<String> {
    if id == 1 { Some("Alice".into()) } else { None }
}

// Must handle both cases
match find_user(42) {
    Some(name) => println!("Found: {}", name),
    None       => println!("Not found"),
}

// Shorthand methods
let name = find_user(1).unwrap_or("unknown".into());
let upper = find_user(1).map(|n| n.to_uppercase());
let len   = find_user(1).as_ref().map(|n| n.len());

// ? operator — propagate None
fn greeting(id: u32) -> Option<String> {
    let name = find_user(id)?;  // returns None if not found
    Some(format!("Hello, {}!", name))
}

17. What is Result<T, E> and how is error handling done in Rust?

Result<T, E> is the standard type for recoverable errors:

use std::fs;
use std::io;

fn read_username() -> Result<String, io::Error> {
    let content = fs::read_to_string("user.txt")?; // ? propagates error
    Ok(content.trim().to_string())
}

// Handling results
match read_username() {
    Ok(name)  => println!("User: {}", name),
    Err(e)    => eprintln!("Error: {}", e),
}

// Chainable methods
let upper = read_username()
    .map(|s| s.to_uppercase())
    .unwrap_or_else(|_| "DEFAULT".into());

18. When should you use panic! vs returning Result?

Use panic! Use Result
Unrecoverable bug (programming error) Expected failure condition
Test failures I/O errors, parse errors
Prototype / POC code Library code
Invariant violated (index out of bounds) User input validation
// panic — for programmer errors
fn divide(a: f64, b: f64) -> f64 {
    if b == 0.0 { panic!("Division by zero!"); }
    a / b
}

// Result — for expected failures
fn parse_age(s: &str) -> Result<u8, String> {
    s.parse::<u8>().map_err(|e| format!("Invalid age: {}", e))
}

19. What is the ? operator?

? is syntactic sugar for "propagate the error if Err/None, otherwise unwrap":

// Without ?
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = match std::fs::read_to_string(path) {
        Ok(s)  => s,
        Err(e) => return Err(Box::new(e)),
    };
    match content.trim().parse::<i32>() {
        Ok(n)  => Ok(n),
        Err(e) => Err(Box::new(e)),
    }
}

// With ? — equivalent, much cleaner
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string(path)?;
    Ok(content.trim().parse::<i32>()?)
}

Memory Management

20. How does Rust manage memory without a garbage collector?

Rust uses ownership + RAII (Resource Acquisition Is Initialization):

  1. Each value has a single owner.
  2. When the owner goes out of scope, the value's Drop impl runs and memory is freed.
  3. No GC pauses, no runtime overhead.
{
    let s = String::from("hello"); // heap allocated
    // use s
} // s.drop() called automatically — heap memory freed

// Custom Drop
struct Connection { id: u32 }
impl Drop for Connection {
    fn drop(&mut self) { println!("Closing connection {}", self.id); }
}

21. What are Box<T>, Rc<T>, and Arc<T>?

Type Ownership Thread-safe Use case
Box<T> Single owner Yes Heap allocation, recursive types
Rc<T> Multiple owners ❌ No Single-threaded shared ownership
Arc<T> Multiple owners ✅ Yes Multi-threaded shared ownership
// Box — heap allocation
let b = Box::new(5);
println!("{}", b);  // auto-derefs

// Rc — reference-counted, single thread
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a);  // ref count: 2
println!("{} {}", a, b);

// Arc — atomic Rc, multi-thread safe
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);
let data2 = Arc::clone(&data);
std::thread::spawn(move || println!("{:?}", data2));

22. What is RefCell<T> and interior mutability?

RefCell<T> allows mutating data through a shared reference — enforcing borrow rules at runtime instead of compile time:

use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);

// Borrow immutably
let r1 = data.borrow();
println!("{:?}", r1);
drop(r1); // must release before mutable borrow

// Borrow mutably
data.borrow_mut().push(4);
println!("{:?}", data.borrow());

// Rc<RefCell<T>> — multiple owners with interior mutability
use std::rc::Rc;
let shared = Rc::new(RefCell::new(0));
let clone = Rc::clone(&shared);
*clone.borrow_mut() += 1;
println!("{}", shared.borrow()); // 1

Concurrency

23. How does Rust achieve fearless concurrency?

Rust's type system makes data races impossible to compile:

  • Send: a type is safe to move to another thread.
  • Sync: a type is safe to share references across threads.

The compiler automatically derives Send/Sync for most types and rejects unsafe patterns:

// ❌ Cannot send Rc across threads — it's not Send
let rc = std::rc::Rc::new(1);
std::thread::spawn(move || println!("{}", rc)); // compile error

// ✅ Arc is Send + Sync
let arc = std::sync::Arc::new(1);
let arc2 = arc.clone();
std::thread::spawn(move || println!("{}", arc2));

24. How do you share mutable state between threads?

Use Arc<Mutex<T>> — atomic reference counting + mutual exclusion:

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let c = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        let mut num = c.lock().unwrap(); // blocks until lock acquired
        *num += 1;
    }));
}

for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // 10

Use RwLock<T> when reads are frequent and writes are rare.

25. What are channels in Rust?

Channels implement message passing — a safe alternative to shared memory:

use std::sync::mpsc; // multiple producer, single consumer
use std::thread;

let (tx, rx) = mpsc::channel();

// Clone sender for multiple producers
let tx2 = tx.clone();

thread::spawn(move || tx.send("hello from thread 1").unwrap());
thread::spawn(move || tx2.send("hello from thread 2").unwrap());

// Receive (blocks until message arrives)
for _ in 0..2 {
    println!("{}", rx.recv().unwrap());
}

Async / Await

26. How does async/await work in Rust?

Rust's async is based on zero-cost Futures — they are state machines compiled at build time, not runtime threads:

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let result = fetch_data().await;
    println!("{}", result);
}

async fn fetch_data() -> String {
    sleep(Duration::from_millis(100)).await;
    "data ready".to_string()
}

Key points:

  • async fn returns impl Future<Output = T>
  • .await suspends the current task, not the thread
  • Requires an async runtime (Tokio, async-std)

27. What is a Future in Rust?

A Future is a value that may not be ready yet — similar to Promise in JS:

trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Futures are lazy — nothing happens until you .await them or pass them to a runtime executor.

// Concurrent futures with tokio::join!
async fn fetch_all() {
    let (a, b, c) = tokio::join!(
        fetch_user(1),
        fetch_posts(1),
        fetch_comments(1),
    );
}

// Parallel tasks
async fn fetch_parallel() {
    let handle1 = tokio::spawn(fetch_user(1));
    let handle2 = tokio::spawn(fetch_posts(1));
    let (u, p) = (handle1.await.unwrap(), handle2.await.unwrap());
}

28. What is Pin<T> and why is it needed?

Pin<T> prevents a value from being moved in memory — required for self-referential structs that async state machines can become:

use std::pin::Pin;
use std::future::Future;

// Most async code doesn't need Pin directly
// The compiler handles it when you use async/await

// Manually pinning (advanced)
let fut = async { 42 };
let mut boxed: Pin<Box<dyn Future<Output = i32>>> = Box::pin(fut);

In practice, you mostly encounter Pin when implementing Future manually or working with tokio::pin!.


Error Handling (Advanced)

29. How do you create custom error types?

use std::fmt;

#[derive(Debug)]
enum AppError {
    NotFound(String),
    ParseError(std::num::ParseIntError),
    IoError(std::io::Error),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::NotFound(s)   => write!(f, "Not found: {}", s),
            AppError::ParseError(e) => write!(f, "Parse error: {}", e),
            AppError::IoError(e)    => write!(f, "IO error: {}", e),
        }
    }
}

// Implement From for automatic ? conversion
impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::ParseError(e) }
}

fn parse_id(s: &str) -> Result<u32, AppError> {
    Ok(s.parse::<u32>()?)  // auto-converts ParseIntError → AppError
}

30. What is the thiserror and anyhow crates used for?

Crate Use case When to use
thiserror Library error types When callers need to match on errors
anyhow Application error handling When you just need to propagate errors
// thiserror — clean derive macro for error types
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
    #[error("File not found: {0}")]
    NotFound(String),
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

// anyhow — ergonomic for application code
use anyhow::{Context, Result};
fn read_config() -> Result<Config> {
    let text = std::fs::read_to_string("config.toml")
        .context("Failed to read config file")?;
    Ok(toml::from_str(&text)?)
}

Structs & Generics

31. What are Rust structs and how do they differ from classes?

Rust structs hold data; behaviour is added through impl blocks and traits — no inheritance:

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    // Associated function (constructor)
    fn new(x: f64, y: f64) -> Self { Point { x, y } }

    // Method — takes &self
    fn distance(&self, other: &Point) -> f64 {
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }

    // Mutable method
    fn translate(&mut self, dx: f64, dy: f64) {
        self.x += dx;
        self.y += dy;
    }
}

let mut p = Point::new(0.0, 0.0);
p.translate(3.0, 4.0);
println!("{:.1}", p.distance(&Point::new(0.0, 0.0))); // 5.0

32. How do generics work in Rust?

Generics are monomorphised at compile time — zero runtime cost:

// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list { if item > largest { largest = item; } }
    largest
}

// Generic struct
struct Pair<T> {
    first: T,
    second: T,
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.first >= self.second {
            println!("largest: {}", self.first);
        } else {
            println!("largest: {}", self.second);
        }
    }
}

33. What are associated types in traits?

Associated types are a way to bind a type to a trait without making the trait generic:

trait Container {
    type Item;       // associated type
    fn get(&self, i: usize) -> Option<&Self::Item>;
    fn len(&self) -> usize;
}

struct Stack<T>(Vec<T>);

impl<T> Container for Stack<T> {
    type Item = T;
    fn get(&self, i: usize) -> Option<&T> { self.0.get(i) }
    fn len(&self) -> usize { self.0.len() }
}

Use associated types when a trait has exactly one logical output type; use generic parameters when multiple implementations for the same type make sense.


Closures & Iterators

34. What are closures in Rust and how do they capture variables?

Closures can capture environment by reference, mutable reference, or by moving ownership:

let x = 5;

// Captures &x (Fn trait)
let print = || println!("{}", x);
print();

// Captures &mut counter (FnMut trait)
let mut counter = 0;
let mut inc = || { counter += 1; counter };
println!("{}", inc()); // 1

// Captures ownership with `move` (FnOnce trait)
let s = String::from("hello");
let consume = move || println!("{}", s);
// s is no longer accessible here
consume();

35. What is the difference between Fn, FnMut, and FnOnce?

Trait Captures Can call
Fn By reference Multiple times
FnMut By mutable reference Multiple times (mutating)
FnOnce By move (ownership) Only once
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
fn apply_mut<F: FnMut(i32) -> i32>(mut f: F, x: i32) -> i32 { f(x) }
fn apply_once<F: FnOnce(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }

36. How do Rust iterators work?

Rust iterators are lazy — adaptors build a pipeline, nothing executes until a consuming method is called:

let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let result: Vec<i32> = numbers.iter()
    .filter(|&&x| x % 2 == 0)   // lazy
    .map(|&x| x * x)             // lazy
    .take(3)                      // lazy
    .collect();                   // consuming — executes everything

println!("{:?}", result); // [4, 16, 36]

// Other consuming methods
let sum: i32 = numbers.iter().sum();
let any_big = numbers.iter().any(|&x| x > 5);
let found = numbers.iter().find(|&&x| x > 5); // Option<&i32>

Advanced Topics

37. What are Rust's smart pointers?

Smart Pointer Description
Box<T> Heap allocation, single owner
Rc<T> Multiple owners (single-threaded)
Arc<T> Multiple owners (thread-safe)
Cell<T> Interior mutability for Copy types
RefCell<T> Interior mutability with runtime borrow check
Mutex<T> Mutual exclusion for threads
RwLock<T> Multiple readers or one writer

38. What is unsafe Rust?

unsafe blocks allow operations that the compiler can't verify are safe:

unsafe fn dangerous() { ... }

let mut v = vec![1, 2, 3];
let r = &mut v[0] as *mut i32;  // raw pointer

unsafe {
    *r = 42;  // dereferencing raw pointer
    dangerous();
}

Five things only unsafe allows:

  1. Dereference raw pointers
  2. Call unsafe functions
  3. Access/modify mutable static variables
  4. Implement unsafe traits
  5. Access fields of unions

The invariants broken by unsafe must be upheld by the programmer — the compiler trusts you.

39. What are macros in Rust?

Rust has two types of macros — they operate on the AST before compilation:

// Declarative macros (macro_rules!)
macro_rules! hello {
    () => { println!("Hello!"); };
    ($name:expr) => { println!("Hello, {}!", $name); };
}
hello!();         // "Hello!"
hello!("Alice");  // "Hello, Alice!"

// Procedural macros (derive macros)
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Config { host: String, port: u16 }

Common built-in macros: vec![], println!, format!, assert!, todo!, unimplemented!, dbg!.

40. What is the newtype pattern?

Wrapping a type in a struct to create a distinct type — adds type safety and allows implementing traits:

struct Meters(f64);
struct Kilograms(f64);

// Can't accidentally add meters and kilograms
fn calculate(distance: Meters, _weight: Kilograms) -> f64 {
    distance.0 / 9.8
}

// Implement Display on external type (orphan rule workaround)
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

41. What is the orphan rule?

You can only implement a trait for a type if either the trait or the type is defined in your crate:

// ✅ Your trait, external type
impl MyTrait for Vec<i32> { ... }

// ✅ External trait, your type
impl Display for MyStruct { ... }

// ❌ Orphan: neither is yours
impl Display for Vec<i32> { ... }  // compile error

The newtype pattern wraps external types to work around this.

42. What is Deref coercion?

Rust automatically dereferences smart pointers when needed:

fn hello(name: &str) { println!("Hello, {}!", name); }

let s = String::from("Alice");
hello(&s);              // &String → &str (Deref coercion)

let b = Box::new(s);
hello(&b);              // &Box<String> → &String → &str (chained)

Deref coercions happen automatically in function calls, method calls, and some other contexts.


Testing

43. How do you write tests in Rust?

// Unit test in same file
fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow() {
        let _: u8 = 200u8 + 100u8; // panics in debug mode
    }

    #[test]
    fn test_result() -> Result<(), String> {
        let x = "5".parse::<i32>().map_err(|e| e.to_string())?;
        assert_eq!(x, 5);
        Ok(())
    }
}
cargo test              # run all tests
cargo test test_add     # run specific test
cargo test -- --nocapture  # show println! output

44. What are integration tests in Rust?

Integration tests live in the tests/ directory and test the public API:

// tests/integration_test.rs
use my_library::add;

#[test]
fn it_adds_two() {
    assert_eq!(add(2, 2), 4);
}
my_project/
├── src/lib.rs
├── tests/
│   └── integration_test.rs
└── Cargo.toml

Cargo & Ecosystem

45. What is Cargo and what are its key commands?

Cargo is Rust's build system and package manager:

Command Description
cargo new project Create new binary project
cargo new --lib lib Create new library crate
cargo build Compile (debug mode)
cargo build --release Optimised build
cargo run Build and run
cargo test Run tests
cargo bench Run benchmarks
cargo check Type-check without building
cargo clippy Lint with Clippy
cargo fmt Format with rustfmt
cargo doc --open Generate and open docs
cargo add serde Add dependency
cargo update Update dependencies
cargo publish Publish to crates.io

46. What is the difference between a binary and library crate?

Feature Binary (src/main.rs) Library (src/lib.rs)
Entry point fn main() No main
Executable Yes No (.rlib / .so)
Published Optional Primary target
Can have both Yes — same Cargo.toml
# Cargo.toml
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

Common Rust Patterns

47. What is the Builder pattern in Rust?

#[derive(Debug)]
struct Config {
    host: String,
    port: u16,
    timeout_ms: u64,
}

struct ConfigBuilder {
    host: String,
    port: u16,
    timeout_ms: u64,
}

impl ConfigBuilder {
    fn new() -> Self {
        ConfigBuilder { host: "localhost".into(), port: 8080, timeout_ms: 5000 }
    }
    fn host(mut self, h: &str) -> Self { self.host = h.into(); self }
    fn port(mut self, p: u16) -> Self { self.port = p; self }
    fn timeout_ms(mut self, t: u64) -> Self { self.timeout_ms = t; self }
    fn build(self) -> Config {
        Config { host: self.host, port: self.port, timeout_ms: self.timeout_ms }
    }
}

let cfg = ConfigBuilder::new()
    .host("example.com")
    .port(443)
    .timeout_ms(3000)
    .build();

48. What is the type state pattern?

Using types to enforce valid state transitions at compile time:

struct Locked;
struct Unlocked;

struct Vault<State> { _state: std::marker::PhantomData<State> }

impl Vault<Locked> {
    fn new() -> Self { Vault { _state: std::marker::PhantomData } }
    fn unlock(self, _pin: &str) -> Vault<Unlocked> {
        Vault { _state: std::marker::PhantomData }
    }
}

impl Vault<Unlocked> {
    fn withdraw(&self, amount: f64) { println!("Withdrew ${}", amount); }
    fn lock(self) -> Vault<Locked> { Vault { _state: std::marker::PhantomData } }
}

let vault = Vault::new();
// vault.withdraw(100.0); // ❌ compile error — vault is Locked
let vault = vault.unlock("1234");
vault.withdraw(100.0);   // ✅
let _vault = vault.lock();

Anti-patterns

49. What are common Rust anti-patterns?

Anti-pattern Problem Better approach
.unwrap() everywhere Panics in production Use ?, map_err, or handle errors
clone() to fix borrow errors Unnecessary heap allocation Restructure ownership or use references
Rc<RefCell<T>> by default Runtime panics, hard to reason about Use ownership + &mut when possible
String parameter instead of &str Forces allocation Accept &str, convert inside if needed
Mutex deadlocks Lock held across await Drop lock before .await
unsafe for convenience Unsound code Look for safe alternatives first
Ignoring clippy warnings Subtle bugs cargo clippy -- -D warnings in CI
Not using #[derive] Boilerplate Derive Debug, Clone, PartialEq

Rust vs Other Languages

50. How does Rust compare to C++, Go, and Python for systems programming?

Feature Rust C++ Go Python
Memory safety Compile-time guaranteed Manual (UB possible) GC GC
Performance C-like Best-in-class Near-C 10–100× slower
Concurrency safety Data races impossible Manual Goroutines (safe) GIL limits
Null safety Option<T> (no nulls) Null pointers nil (runtime panic) None (runtime)
Error handling Result<T,E> Exceptions / error codes Multiple returns Exceptions
Compile time Slower (complex checks) Slow (templates) Fast N/A (interpreted)
Learning curve Steep (ownership/borrow) Steep (UB, templates) Gentle Very gentle
Async model Futures (zero-cost) coroutines (C++20) Goroutines asyncio
Ecosystem Growing (crates.io) Mature Mature (stdlib) Largest
Best for Systems, WASM, embedded Games, OS, HFT Cloud, CLIs, APIs ML, scripting

Common mistakes

Mistake Why it's wrong Fix
Using String when &str suffices Forces heap allocation Accept &str, return String only if owned
Holding MutexGuard across .await Deadlock in async code Drop guard before await point
Using index[] on slices/vecs Panics on out-of-bounds Use .get() returning Option
Cloning to satisfy borrow checker Hides design issues Restructure lifetimes or ownership
Arc<Mutex<>> when not needed Unnecessary overhead Use &mut or channels instead
Implementing Drop on Copy types Compile error Remove Copy impl or remove Drop
Forgetting mut on function params Can't mutate inside function Add mut to binding
Using 'static lifetime everywhere Overly restrictive Use the correct lifetime parameter

Rust vs alternatives

Language Memory Speed Safety Ergonomics Best use
Rust Manual (ownership) ~C Compile-time Moderate Systems, WASM, embedded
C Manual (unsafe) Fastest None Low OS kernels, drivers
C++ Manual/smart ptrs ~Rust Partial Moderate Games, HFT, HPC
Go GC ~2-5× slower Runtime High Cloud, APIs, DevOps
Java/JVM GC ~3-10× slower Runtime High Enterprise, Android
Python GC ~50-100× slower Runtime Very high ML, scripting, data
Zig Manual (no RAII) ~Rust Partial Moderate Systems (simpler than Rust)

FAQ

Q: Is Rust's learning curve worth it?
The ownership/borrow checker is challenging but eliminates whole classes of bugs: buffer overflows, use-after-free, data races, null dereferences. Once it clicks, you write safer code in all languages. Teams report fewer production bugs and confident refactoring.

Q: When should I use async Rust vs threads?
Use async for I/O-bound work with high concurrency (thousands of connections). Use threads for CPU-bound parallelism. Mix both: tokio::task::spawn_blocking runs blocking code in a thread pool without blocking the async runtime.

Q: What is the difference between String and &str?
String is an owned, heap-allocated, growable UTF-8 string. &str is a borrowed slice of UTF-8 bytes — it can point into a String, a string literal (stored in binary), or any UTF-8 buffer. Prefer &str for function parameters; return String when the function creates the data.

Q: What is monomorphisation and does it cause code bloat?
Monomorphisation means the compiler generates a separate copy of generic code for each concrete type used. This enables zero-cost abstractions but can increase binary size. Use dyn Trait to avoid monomorphisation where code size matters more than performance.

Q: How do I avoid fighting the borrow checker?

  1. Prefer smaller, shorter-lived borrows. 2. Return owned values instead of references when possible. 3. Use .clone() initially, optimise later. 4. Restructure code so borrows don't overlap. 5. Use split_at_mut for multiple mutable slices. 6. Read the error messages — they are excellent.

Q: What Rust crates should I know for backend development?
tokio (async runtime), axum or actix-web (web framework), sqlx or diesel (database), serde / serde_json (serialisation), reqwest (HTTP client), tracing (observability), thiserror / anyhow (error handling), tower (middleware), clap (CLI parsing).

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools