Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 078 / 100

Rust interview questions: a hands-on guide for engineers

Questions for Rust roles.

Expona Admin8 min read

Rust interview questions: a hands-on guide for engineers

When you step into a Rust role, interviewers aren't just testing syntax knowledge—they're evaluating whether you understand the language's core philosophy around memory safety and concurrency. Rust interview questions rarely ask you to recite documentation; instead, they probe your grasp of ownership, borrowing, thread safety, and error handling through scenarios you'll actually face in production. This guide walks you through the most common Rust interview questions that mid-level and senior engineers encounter, with hands-on examples that clarify not just the "what" but the "why."

Understanding ownership

Ownership is the bedrock of Rust's memory model. Every value in Rust has exactly one owner, and when that owner leaves scope, the value is dropped automatically. This eliminates the need for garbage collection while preventing use-after-free bugs. In interviews, you'll often get asked to explain ownership in the context of moving data or returning values from functions.

Let's consider a real scenario: you're building a web service that processes user profiles. Each profile struct is owned by one variable at a time.

rust
struct UserProfile {
    id: u64,
    name: String,
    email: String,
}

fn process_profile(profile: UserProfile) {
    println!("Processing user: {}", profile.name);
    // ownership of profile moves into this function
    // profile is dropped here when the function ends
}

fn main() {
    let user = UserProfile {
        id: 1,
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
    };

    process_profile(user);
    // user no longer exists here—ownership was moved
    // This line would fail: println!("{}", user.name);
}

The key insight: when you pass user to process_profile, ownership transfers completely. The original variable can no longer be used. Interviewers expect you to recognize this and explain why Rust does this—it prevents double-free errors and makes resource cleanup predictable.

When asked about ownership in an interview, mention that it forces you to think about data flow. You can't accidentally use a value after it's been freed because the compiler catches it at compile time. This is why Rust code is often safer than C or C++ without requiring a runtime garbage collector.

The borrowing rules

Borrowing lets you reference data without taking ownership. Rust enforces two simple rules: you can have either many immutable references to the same data, or exactly one mutable reference. This prevents data races at compile time. Interview questions often test whether you can spot violations of these rules or explain why a piece of code won't compile.

Here's a common scenario from e-commerce systems where multiple components need to read the same shopping cart data:

rust
struct ShoppingCart {
    items: Vec<String>,
    total: f64,
}

fn display_items(cart: &ShoppingCart) {
    // immutable borrow: we can only read
    for item in &cart.items {
        println!("{}", item);
    }
}

fn apply_discount(cart: &mut ShoppingCart, discount: f64) {
    // mutable borrow: we can modify
    cart.total -= discount;
}

fn main() {
    let mut cart = ShoppingCart {
        items: vec![String::from("Laptop"), String::from("Mouse")],
        total: 1500.0,
    };

    // Multiple immutable borrows are fine
    display_items(&cart);
    display_items(&cart);

    // Mutable borrow requires exclusive access
    apply_discount(&mut cart, 100.0);

    // Can't do this: mutable borrow while immutable exists
    // let read_ref = &cart;
    // apply_discount(&mut cart, 50.0);  // error!
}

Interviewers want to see that you understand why this rule exists. Data races happen when one thread modifies data while another reads it. By preventing simultaneous mutable and immutable references, Rust eliminates this class of bug entirely. When explaining borrowing in an interview, emphasize that the compiler is your ally—if code doesn't compile due to borrowing violations, that's a safety guarantee, not an inconvenience.

A common follow-up question: "How do you work around borrowing limitations?" The answer usually involves clone() for independent copies, interior mutability patterns like RefCell or Mutex for shared mutable state, or redesigning ownership structure. Know at least one pattern well.

Send and Sync traits

These marker traits define thread safety in Rust. Send means a type can be safely moved across thread boundaries; Sync means a type can be safely shared (referenced) across threads. Most types implement both automatically, but understanding when and why they don't is important for systems and concurrent programming roles.

Consider a CRM system that processes customer records in parallel:

rust
use std::thread;
use std::rc::Rc;
use std::sync::Arc;

struct Customer {
    id: u64,
    name: String,
}

fn process_customers_with_rc(customers: Vec<Customer>) {
    // This won't work because Rc is not Send
    // let shared = Rc::new(customers);
    // thread::spawn(move || {
    //     println!("{:?}", shared);
    // });
}

fn process_customers_with_arc(customers: Vec<Customer>) {
    // Arc is both Send and Sync, so this works
    let shared = Arc::new(customers);
    
    let shared_clone = Arc::clone(&shared);
    let handle = thread::spawn(move || {
        println!("Thread owns Arc");
    });
    
    handle.join().unwrap();
}

Rc (reference counting) is not Send because its internal reference count isn't atomic—if two threads tried to decrement the count simultaneously, the count could become corrupted. Arc (atomic reference counting) uses atomic operations, so it's safe to send across threads.

In an interview, explain that Send and Sync are compile-time guarantees. You rarely implement them manually; the compiler derives them for you. But knowing what breaks them (like non-atomic counters or Cell) shows you understand the threading model. Senior roles often test this because incorrect assumptions about thread safety cause production bugs.

Result vs panic

This is about error handling philosophy. Result<T, E> represents recoverable errors that the caller might handle. panic! is for unrecoverable errors or programmer bugs. Choosing the right one is crucial for building reliable systems. Interview questions often ask: "When would you panic versus return a Result?"

Here's a practical example from a payment processing system:

rust
use std::num::ParseIntError;

#[derive(Debug)]
enum PaymentError {
    InvalidAmount(String),
    ProcessingFailed(String),
    InsufficientFunds,
}

// Returns Result for expected failure modes
fn validate_and_process_payment(amount_str: &str, balance: f64) -> Result<f64, PaymentError> {
    let amount: f64 = amount_str
        .parse()
        .map_err(|_| PaymentError::InvalidAmount(format!("'{}' is not a valid amount", amount_str)))?;

    if amount <= 0.0 {
        return Err(PaymentError::InvalidAmount(String::from("Amount must be positive")));
    }

    if amount > balance {
        return Err(PaymentError::InsufficientFunds);
    }

    Ok(balance - amount)
}

// Panics for bugs in our own logic
fn calculate_fee_percentage(tier: &str) -> f64 {
    match tier {
        "standard" => 0.02,
        "premium" => 0.01,
        "enterprise" => 0.005,
        other => panic!("Unknown tier: {} — this should never happen", other),
    }
}

fn main() {
    // Caller decides how to handle payment errors
    match validate_and_process_payment("abc", 1000.0) {
        Ok(new_balance) => println!("Payment succeeded, new balance: {}", new_balance),
        Err(PaymentError::InvalidAmount(msg)) => eprintln!("Invalid input: {}", msg),
        Err(PaymentError::InsufficientFunds) => eprintln!("Declined: insufficient funds"),
        Err(PaymentError::ProcessingFailed(msg)) => eprintln!("Processing error: {}", msg),
    }

    // Panic for programmer error—we control the tier values
    let fee = calculate_fee_percentage("standard");
}

The rule: use Result for anything the caller might reasonably handle—network failures, invalid input, missing resources. Use panic! only for bugs that indicate broken invariants in your code. Interviewers want to see that you use unwrap() and expect() judiciously, only where you're truly certain the operation can't fail.

A senior engineer explains error handling by discussing error propagation strategies: matching, the ? operator, map_err for transformation. Show that you think about what the caller can do with the error information you provide.

Common mistakes

Several patterns trip up candidates in Rust interviews:

  • Forgetting that borrowing rules apply to function parameters. When you pass a reference to a function, you still can't create a mutable borrow of the same data elsewhere. The compiler enforces this across function boundaries.
  • Panicking in library code. If you're writing a library or service component, returning Result gives callers the choice of how to handle errors. Panicking removes their agency and can crash the whole system.
  • Misunderstanding Clone. Clone makes a deep copy; it's not free. In performance-critical code, ask yourself if you really need to clone or if borrowing would work.
  • Assuming Sync for shared mutable state. A type can be Sync but still require synchronization (like Mutex) to actually safely share mutable data. Sync just means the type is thread-safe; you still need to use appropriate primitives.
  • Overusing unwrap in production code. It's fine in tests or examples, but production services should handle errors gracefully.

Key takeaways

  • Ownership forces you to think about data flow early; the compiler prevents entire classes of memory bugs.
  • The borrowing rules—many immutable references or one mutable—eliminate data races at compile time.
  • Send and Sync are compile-time guarantees about thread safety; know why Rc isn't Send but Arc is.
  • Result for recoverable errors gives callers control; panic only for programmer bugs.
  • Interviewers test these concepts through code review scenarios and "will this compile?" questions—practice both.
  • Demonstrate that you write Rust not just to pass the compiler, but to make your code easier to reason about and safer in production.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles