← Back to DevBytes

Top 50 Rust Interview Questions for Entry-Level Developers

Top 50 Rust Interview Questions for Entry-Level Developers

Rust has rapidly become one of the most loved programming languages, prized for its memory safety, zero-cost abstractions, and fearless concurrency. For entry-level developers preparing for Rust interviews, mastering both conceptual knowledge and practical coding skills is essential. This tutorial walks through the top 50 most commonly asked Rust interview questions, complete with explanations and code examples to help you land your first Rust developer role.

Why Rust Matters

Rust solves problems that have plagued systems programming for decades: memory leaks, data races, and undefined behavior. Its unique ownership model enforces safety at compile time without a garbage collector, making it ideal for performance-critical applications like web servers, game engines, operating systems, and blockchain infrastructure. Companies like Mozilla, Microsoft, Google, Amazon, and Cloudflare actively use Rust in production, driving strong demand for Rust developers.

Section 1: Rust Basics (Questions 1–10)

Q1. What is Rust and what makes it different from other languages?

Rust is a systems programming language focused on safety, speed, and concurrency. Unlike C/C++, Rust guarantees memory safety through its ownership system without needing a garbage collector. Unlike Java or Python, Rust offers zero-cost abstractions and compiles to native machine code.

Q2. What is the difference between let, let mut, and const?

In Rust, variables are immutable by default. You use mut to make them mutable, and const for compile-time constants that must have explicit types.

let x = 5;          // immutable
let mut y = 10;     // mutable
const MAX: u32 = 100; // constant, must have type

// x = 6;  // ERROR: cannot assign to immutable variable
y = 20;    // OK

Q3. What are the primitive data types in Rust?

Rust has scalar types (integers, floats, booleans, characters) and compound types (tuples, arrays).

let a: i32 = 42;          // signed 32-bit integer
let b: u64 = 100;         // unsigned 64-bit integer
let c: f64 = 3.14;        // 64-bit float
let d: bool = true;       // boolean
let e: char = 'R';        // Unicode character
let tuple: (i32, f64) = (1, 2.5);
let arr: [i32; 3] = [1, 2, 3];

Q4. What is shadowing in Rust?

Shadowing allows you to declare a new variable with the same name as a previous one, effectively replacing it. The new variable can even have a different type.

let x = 5;
let x = x + 1;       // shadows previous x
let x = "hello";     // shadows again, now a &str

println!("{}", x);   // prints "hello"

Q5. What is the difference between String and &str?

String is a heap-allocated, growable string owned by the variable. &str is a reference to a string slice, which can point to heap or static memory. Use String when you need ownership and mutation; use &str when you only need to read.

let s1: String = String::from("hello");
let s2: &str = "world";          // string literal, &'static str
let s3: &str = &s1;              // borrow from String

Q6. How do you print formatted output in Rust?

Rust uses the println! macro with format placeholders.

let name = "Alice";
let age = 30;
println!("Name: {}, Age: {}", name, age);
println!("{name} is {age} years old"); // inline syntax (Rust 2021+)
println!("{:#?}", (1, 2, 3));          // pretty-print debug

Q7. What is the difference between a statement and an expression in Rust?

Statements perform actions and return nothing (like let assignments). Expressions evaluate to a value. Blocks are expressions in Rust.

let y = {
    let x = 3;
    x + 1   // expression, no semicolon — this is the block's value
};
// y is 4

Q8. What are functions in Rust and how do you define them?

Functions are declared with fn, parameters need type annotations, and return types use ->.

fn add(a: i32, b: i32) -> i32 {
    a + b  // implicit return (no semicolon)
}

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

Q9. What is the main function in Rust?

The main function is the entry point of every Rust executable program. It takes no arguments and returns nothing (or a Result in newer versions).

fn main() {
    println!("Program starts here");
}

Q10. How do comments work in Rust?

Rust supports line comments with //, block comments with /* */, and documentation comments with /// or //!.

// Line comment
/* Block
   comment */

/// Documentation for a function
fn documented() {}

//! Module-level documentation

Section 2: Ownership, Borrowing, and References (Questions 11–20)

Q11. What is ownership in Rust?

Ownership is Rust's core memory management concept. Every value has exactly one owner, and when the owner goes out of scope, the value is dropped. This eliminates the need for a garbage collector.

fn main() {
    let s = String::from("hello");  // s owns the String
    takes_ownership(s);
    // s is no longer valid here

    let x = 5;                      // i32 is Copy
    makes_copy(x);
    println!("{}", x);              // still valid
}

fn takes_ownership(s: String) {}
fn makes_copy(x: i32) {}

Q12. What is borrowing in Rust?

Borrowing lets you access a value without taking ownership, using references. You can have either one mutable reference or any number of immutable references at a time.

fn main() {
    let mut s = String::from("hello");
    let len = calculate_length(&s);  // immutable borrow
    change(&mut s);                  // mutable borrow
    println!("'{}' has length {}", s, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

fn change(s: &mut String) {
    s.push_str(", world");
}

Q13. What are the borrowing rules in Rust?

There are two key rules:

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

A dangling reference points to memory that has been freed. Rust's compiler prevents this at compile time.

// This won't compile:
// fn dangle() -> &String {
//     let s = String::from("hello");
//     &s  // s is dropped when function ends — dangling!
// }

// Correct version:
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // ownership moves out, no dangling
}

Q15. What is the difference between a reference and a smart pointer?

References borrow data they don't own. Smart pointers like Box, Rc, and RefCell own data and provide additional capabilities like heap allocation, reference counting, or interior mutability.

let b = Box::new(5);     // heap-allocated
println!("b = {}", b);   // dereferenced automatically

Q16. What is the Copy trait?

Types that implement Copy are copied instead of moved when assigned or passed to functions. All primitive types implement Copy. Types that contain heap data (like String) cannot be Copy.

let x = 5;
let y = x;   // x is copied, both valid
println!("{} {}", x, y);

let s1 = String::from("hi");
let s2 = s1; // s1 is moved, no longer valid
// println!("{}", s1); // ERROR

Q17. What is a slice in Rust?

A slice is a reference to a contiguous sequence of elements in a collection. Slices are borrowed views, not owners.

let s = String::from("hello world");
let hello = &s[0..5];    // &str slice
let world = &s[6..11];

let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3];  // &[i32]

Q18. How does Rust handle memory deallocation?

Rust automatically calls the Drop trait's drop method when a value's owner goes out of scope. This is deterministic — you know exactly when memory is freed.

struct Custom {
    name: String,
}

impl Drop for Custom {
    fn drop(&mut self) {
        println!("Dropping {}", self.name);
    }
}

fn main() {
    let _c = Custom { name: String::from("test") };
    // "Dropping test" printed when _c goes out of scope
}

Q19. What is the difference between move and copy semantics?

Move semantics transfer ownership — the original variable becomes invalid. Copy semantics duplicate the value — both variables remain valid. Moves happen for non-Copy types; copies happen for Copy types.

Q20. Can you have multiple mutable references in Rust?

Not simultaneously. However, you can have sequential mutable references as long as the previous one is no longer used.

let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str("!");
let r2 = &mut s;  // OK, r1 is no longer used
r2.push_str(" world");

Section 3: Data Structures and Enums (Questions 21–28)

Q21. What is a struct in Rust?

A struct groups related fields together. Rust has named-field structs, tuple structs, and unit structs.

struct User {
    name: String,
    age: u32,
}

struct Point(i32, i32);          // tuple struct
struct AlwaysEqual;              // unit struct

let user = User { name: String::from("Bob"), age: 25 };
let p = Point(1, 2);

Q22. How do you implement methods on structs?

Use the impl block. Methods take self as the first parameter; associated functions (like constructors) do not.

struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
    fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }
    }
}

let r = Rectangle::new(10.0, 5.0);
println!("Area: {}", r.area());

Q23. What is an enum in Rust?

Enums allow you to define a type by enumerating its possible variants. Rust enums are algebraic data types — variants can hold data.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

let m = Message::Write(String::from("hello"));

Q24. What is Option<T> and why is it important?

Option<T> is Rust's way of handling nullable values safely. It has two variants: Some(T) and None. It eliminates null pointer dereferences at compile time.

fn find_even(nums: &[i32]) -> Option<i32> {
    for &n in nums {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    None
}

match find_even(&[1, 3, 5, 4]) {
    Some(n) => println!("Found: {}", n),
    None => println!("No even number"),
}

Q25. What is pattern matching and how does match work?

Pattern matching compares a value against patterns and runs code based on which pattern matches. match must be exhaustive.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Q26. What is if let and when would you use it?

if let is syntactic sugar for matching a single pattern, useful when match would be verbose.

let some_value = Some(5);

if let Some(n) = some_value {
    println!("Got {}", n);
} else {
    println!("Got nothing");
}

Q27. What is the difference between a tuple and an array?

Tuples can hold different types and have a fixed length. Arrays hold the same type and have a fixed length known at compile time.

let tuple: (i32, f64, &str) = (1, 2.5, "hi");
let array: [i32; 3] = [1, 2, 3];

Q28. What is a vector and how does it differ from an array?

Vec<T> is a growable, heap-allocated array. Arrays have fixed size; vectors can grow or shrink at runtime.

let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);

let v2 = vec![1, 2, 3]; // macro shorthand

for n in &v {
    println!("{}", n);
}

Section 4: Error Handling (Questions 29–35)

Q29. What is Result<T, E> in Rust?

Result is an enum with Ok(T) and Err(E) variants. It's used for operations that can fail, making error handling explicit.

use std::fs::File;

fn open_file() -> Result<File, std::io::Error> {
    File::open("hello.txt")
}

match open_file() {
    Ok(file) => println!("Opened!"),
    Err(e) => println!("Error: {}", e),
}

Q30. What is the ? operator?

The ? operator returns early with an error if the Result is Err, or unwraps the Ok value. It simplifies error propagation.

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

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

Q31. What is the difference between panic! and Result?

panic! is for unrecoverable errors — it crashes the program. Result is for recoverable errors that the caller should handle. Use panic! for bugs and invariant violations; use Result for expected failures.

// Unrecoverable
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Cannot divide by zero!");
    }
    a / b
}

// Recoverable
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("division by zero"))
    } else {
        Ok(a / b)
    }
}

Q32. How do you create custom error types?

You can define your own error enum and implement the Error trait, or use the thiserror crate for convenience.

use std::fmt;

#[derive(Debug)]
enum AppError {
    NotFound,
    Unauthorized,
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::NotFound => write!(f, "Not found"),
            AppError::Unauthorized => write!(f, "Unauthorized"),
        }
    }
}

impl std::error::Error for AppError {}

Q33. What is unwrap and expect?

Both extract the Ok value or panic on Err. expect lets you provide a custom panic message. Use them in prototypes or when you're certain the result is Ok.

let x: Result<i32, &str> = Ok(42);
let val = x.unwrap();           // panics if Err
let val2 = x.expect("must be ok"); // panics with message

Q34. How do you convert between error types?

Use the From trait to convert one error type into another, enabling the ? operator to work across error types.

use std::io;
use std::num::ParseIntError;

#[derive(Debug)]
enum MyError {
    Io(io::Error),
    Parse(ParseIntError),
}

impl From<io::Error> for MyError {
    fn from(e: io::Error) -> Self {
        MyError::Io(e)
    }
}

impl From<ParseIntError> for MyError {
    fn from(e: ParseIntError) -> Self {
        MyError::Parse(e)
    }
}

Q35. What is the anyhow crate?

anyhow is a popular crate for application-level error handling. It provides a simple anyhow::Result type that can hold any error, making it easy to propagate errors without defining custom error types.

use anyhow::{Context, Result};

fn read_config() -> Result<String> {
    let content = std::fs::read_to_string("config.toml")
        .context("Failed to read config file")?;
    Ok(content)
}

Section 5: Traits and Generics (Questions 36–42)

Q36. What is a trait in Rust?

A trait defines shared behavior that types can implement. It's similar to interfaces in other languages.

trait Summary {
    fn summarize(&self) -> String;
}

struct Article {
    title: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}: {}", self.title, self.content)
    }
}

Q37. What are default trait methods?

Traits can provide default implementations that types can override or use as-is.

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

    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

struct Person { n: String }

impl Greet for Person {
    fn name(&self) -> String { self.n.clone() }
    // greet() uses default implementation
}

Q38. What are generics in Rust?

Generics allow you to write code that works with multiple types. Rust uses monomorphization — generating specific code for each type at compile time — so generics have zero runtime cost.

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

println!("{}", largest(&[1, 5, 3]));    // 5
println!("{}", largest(&['a', 'z', 'm'])); // z

Q39. What are trait bounds?

Trait bounds specify which traits a generic type must implement. They ensure the generic code can use the required methods.

// Using where clause
fn print_info<T>(item: &T)
where
    T: std::fmt::Debug + std::fmt::Display,
{
    println!("Display: {}", item);
    println!("Debug: {:?}", item);
}

// Using impl Trait syntax
fn process(item: &impl Summary) -> String {
    item.summarize()
}

Q40. What is the difference between impl Trait and generics?

impl Trait in parameter position is syntactic sugar for a generic with a trait bound. In return position, it returns an anonymous type implementing the trait, useful for closures and iterators.

// These are equivalent:
fn foo(x: impl Display) {}
fn foo<T: Display>(x: T) {}

// Return position — can't do this with named generics easily:
fn make_iter() -> impl Iterator<Item = i32> {
    (1..10).filter(|x| x % 2 == 0)
}

Q41. What is trait object and dynamic dispatch?

Trait objects (&dyn Trait or Box<dyn Trait>) allow runtime polymorphism. Unlike generics (static dispatch), trait objects use dynamic dispatch via a vtable.

trait Animal {
    fn sound(&self) -> String;
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn sound(&self) -> String { String::from("Woof") }
}

impl Animal for Cat {
    fn sound(&self) -> String { String::from("Meow") }
}

let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog),
    Box::new(Cat),
];

for a in &animals {
    println!("{}", a.sound());
}

Q42. What are derived traits?

Derived traits are automatically implemented using the #[derive] attribute. Common ones include Debug, Clone, Copy, PartialEq, Eq, Hash, and Default.

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

let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();
assert_eq!(p1, p2);

Section 6: Lifetimes and Closures (Questions 43–46)

Q43. What are lifetimes in Rust?

Lifetimes are the compiler's way of ensuring references are valid as long as they're used. Most lifetimes are inferred, but sometimes you must annotate them explicitly.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

let result = longest("hello", "world!");
println!("{}", result);

Q44. What is the static lifetime?

'static means the reference is valid for the entire program duration. All string literals have the 'static lifetime because they're stored in the binary's read-only data segment.

let s: &'static str = "I live forever";

Q45. What is a closure in Rust?

A closure is an anonymous function that can capture variables from its environment. Closures are written with |params| body syntax.

let add = |a, b| a + b;
println!("{}", add(1, 2));  // 3

let x = 10;
let add_x = |y| x + y;      // captures x
println!("{}", add_x(5));   // 15

Q46. How do closures capture variables?

Closures capture by reference (&T), mutable reference (&mut T), or by value (moving ownership). Rust infers the capture mode, but you can force a move with the move keyword.

let s = String::from("hello");

let borrow = || println!("{}", s);          // borrows
let move_closure = move || println!("{}", s); // moves s

borrow();
move_closure();
// borrow(); // s still valid here since borrow only borrowed

Section 7: Concurrency, Modules, and Ecosystem (Questions 47–50)

Q47. How does Rust handle concurrency?

Rust prevents data races at compile time through its ownership and borrowing rules. The Send trait marks types safe to transfer between threads, and Sync marks types safe to share between threads.

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

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

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

    for h in handles {
        h.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Q48. What is the module system in Rust?

Rust's module system organizes code into modules with mod. Use pub to make items public. The use keyword brings paths into scope.

// In lib.rs
mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    fn private_fn() {} // not accessible outside
}

use math::add;

fn main() {
    println!("{}", add(1, 2));
}

Q49. What is Cargo and how do you use it?

Cargo is Rust's build system and package manager. It handles dependencies, compilation, testing, and documentation.

// Create a new project
// cargo new my_project

// Add dependencies in Cargo.toml:
// [dependencies]
// serde = { version = "1.0", features = ["derive"] }

// Common commands:
// cargo build       - compile the project
// cargo run         - build and run
// cargo test        - run tests
// cargo doc --open  - generate and open docs

Q50. How do you write tests in Rust?

Rust has built-in testing support. Use the #[test] attribute for unit tests and assert!, assert_eq!, and assert_ne! macros for assertions.

pub 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]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    #[should_panic]
    fn test_panic() {
        panic!("expected panic");
    }
}

Best Practices for Rust Interviews

Conclusion

Preparing for a Rust interview as an entry-level developer means building a solid understanding of ownership, borrowing, lifetimes, error handling, traits, and concurrency. The 50 questions covered in this tutorial represent the core knowledge that interviewers expect, from basic syntax to the language's most distinctive features. By studying these concepts, writing practice code, and understanding the reasoning behind Rust's design decisions, you'll be well-equipped to demonstrate both technical competence and thoughtful engineering judgment. Remember that interviewers care not just about whether your code compiles, but about whether you understand why Rust enforces its rules — that deeper understanding is what separates strong candidates from the rest. Keep building projects, contribute to open source, and let your curiosity guide you deeper into the Rust ecosystem.

— Ad —

Google AdSense will appear here after approval

← Back to all articles