All topics
Frontend · Learning hub

Rust notes for developers

Master Rust with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
Rust

Core Language & Ownership

Core Language & Ownership Basics // Variables (immutable by default!) let x = 5; let mut y = 10; // mutable y += 1; // Type annotations let n: i32 = 42; let f:

Core Language & Ownership

Basics

// Variables (immutable by default!)
let x = 5;
let mut y = 10;   // mutable
y += 1;

// Type annotations
let n: i32 = 42;
let f: f64 = 3.14;
let b: bool = true;
let s: &str = "hello";          // string slice (borrowed)
let owned: String = String::from("hello");  // owned String

// Shadowing (re-declare, can change type)
let x = 5;
let x = x * 2;  // x is now 10
let x = "now a string";

// Constants (always typed, evaluated at compile time)
const MAX_SIZE: usize = 1024;

// Primitive types
// Integers: i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize
// Floats: f32 f64
// bool, char (Unicode, 4 bytes)

// Tuples
let tup: (i32, f64, bool) = (500, 6.4, true);
let (a, b, c) = tup;   // destructure
let first = tup.0;

// Arrays (fixed size, same type)
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10];   // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

// Vectors (dynamic)
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
let v2 = vec![1, 2, 3];

Ownership & Borrowing

Rust's ownership system eliminates data races and memory leaks at compile time — no garbage collector, no manual free().

// Rules:
// 1. Each value has exactly one owner
// 2. When the owner goes out of scope, the value is dropped (free'd)
// 3. There can only be one mutable reference OR many immutable references (not both)

// Move semantics
let s1 = String::from("hello");
let s2 = s1;     // s1 is MOVED to s2 — s1 is no longer valid
// println!("{}", s1);  // COMPILE ERROR: s1 moved

// Clone (deep copy)
let s1 = String::from("hello");
let s2 = s1.clone();  // s1 still valid

// Copy trait — primitives are copied, not moved
let x = 5;
let y = x;   // x is still valid (i32 implements Copy)

// References (borrowing) — doesn't take ownership
fn calculate_length(s: &String) -> usize {  // borrow s
    s.len()
}
let s = String::from("hello");
let len = calculate_length(&s);  // pass reference
// s is still valid here

// Mutable references
fn change(s: &mut String) {
    s.push_str(" world");
}
let mut s = String::from("hello");
change(&mut s);

// RULE: only one mutable reference at a time (prevents data races)
// let r1 = &mut s;
// let r2 = &mut s;  // COMPILE ERROR
// println!("{} {}", r1, r2);

// Slices — reference to part of a collection
let s = String::from("hello world");
let hello = &s[0..5];   // "hello"
let world = &s[6..11];  // "world"

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

Structs, Enums & Pattern Matching

// Struct
#[derive(Debug, Clone)]
struct User {
    username: String,
    email: String,
    age: u32,
    active: bool,
}

impl User {
    // Constructor convention
    fn new(username: String, email: String) -> User {
        User { username, email, age: 0, active: true }
    }
    // Method — borrows self
    fn greet(&self) -> String {
        format!("Hello, {}!", self.username)
    }
    // Mutable method
    fn deactivate(&mut self) {
        self.active = false;
    }
}

// Enum
#[derive(Debug)]
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

// Pattern matching — exhaustive
fn process(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to {x},{y}"),
        Message::Write(text) => println!("Write: {text}"),
        Message::ChangeColor(r, g, b) => println!("Color: {r},{g},{b}"),
    }
}

// if let — single-variant match
if let Message::Write(text) = msg {
    println!("Got text: {text}");
}
Rust

Error Handling, Traits & Async

Error Handling, Traits & Async Error Handling with Result & Option // Option<T> — value may or may not exist fn find_user(id: u32) -> Option<String> { if id ==

Error Handling, Traits & Async

Error Handling with Result & Option

// Option<T> — value may or may not exist
fn find_user(id: u32) -> Option<String> {
    if id == 1 { Some(String::from("Alice")) }
    else { None }
}

match find_user(1) {
    Some(name) => println!("Found: {name}"),
    None => println!("Not found"),
}

// Shorthand
let name = find_user(1).unwrap();              // panics if None
let name = find_user(1).expect("User not found"); // panic with message
let name = find_user(1).unwrap_or("Unknown".to_string());
let len = find_user(1).map(|n| n.len());       // Option<usize>
let name = find_user(1)?;                      // propagate None as return

// Result<T, E> — success or error
use std::fs;
use std::io;

fn read_file(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?;  // ? propagates error
    Ok(content)
}

match read_file("config.txt") {
    Ok(content) => println!("{content}"),
    Err(e) => eprintln!("Error: {e}"),
}

// ? operator — early return on error
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
    let n: i32 = s.trim().parse()?;  // propagate ParseIntError
    Ok(n * 2)
}

// anyhow for application error handling
use anyhow::{Result, Context};
fn run() -> Result<()> {
    let data = fs::read_to_string("config.json")
        .context("Failed to read config")?;
    Ok(())
}

Traits

// Trait — like an interface
trait Greet {
    fn greet(&self) -> String;
    fn farewell(&self) -> String {   // default implementation
        String::from("Goodbye!")
    }
}

struct English;
struct Spanish;

impl Greet for English {
    fn greet(&self) -> String { String::from("Hello!") }
}

impl Greet for Spanish {
    fn greet(&self) -> String { String::from("¡Hola!") }
    fn farewell(&self) -> String { String::from("¡Adiós!") }
}

// Trait as parameter
fn say_hello(lang: &impl Greet) {
    println!("{}", lang.greet());
}

// Trait bound syntax (equivalent)
fn say_hello<T: Greet>(lang: &T) {
    println!("{}", lang.greet());
}

// Dynamic dispatch (trait object — runtime polymorphism)
fn say_hellos(langs: &[Box<dyn Greet>]) {
    for lang in langs {
        println!("{}", lang.greet());
    }
}

// Derive common traits
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
struct Config { ... }

// Common standard traits
// Display — for println!("{}", val)
// From/Into — type conversion
// Iterator — enables for loops and iterator adapters
// Send/Sync — thread safety markers

Async/Await with Tokio

// Cargo.toml
// tokio = { version = "1", features = ["full"] }
// reqwest = { version = "0.12", features = ["json"] }

use tokio;
use reqwest;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let response = fetch_user(1).await?;
    println!("{:?}", response);
    Ok(())
}

async fn fetch_user(id: u32) -> anyhow::Result<serde_json::Value> {
    let url = format!("https://jsonplaceholder.typicode.com/users/{}", id);
    let resp = reqwest::get(&url).await?.json().await?;
    Ok(resp)
}

// Parallel tasks
use tokio::task;

let (r1, r2) = tokio::join!(
    fetch_user(1),
    fetch_user(2),
);

// Spawn concurrent task
let handle = tokio::spawn(async move {
    // runs concurrently
    heavy_async_work().await
});
let result = handle.await?;   // join

// Channels
use tokio::sync::mpsc;
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move { tx.send("hello").await.unwrap(); });
while let Some(msg) = rx.recv().await {
    println!("{msg}");
}
Rust

Interview Questions

Rust Interview Questions Q: What is the borrow checker and why does Rust have it? The borrow checker enforces ownership rules at compile time, preventing: use-a

Rust Interview Questions

Q: What is the borrow checker and why does Rust have it?

The borrow checker enforces ownership rules at compile time, preventing: use-after-free (using memory after it's freed), dangling pointers (pointers to freed memory), data races (concurrent mutable access). These are the root causes of most memory safety bugs in C/C++. Rust catches them at compile time with zero runtime cost — no garbage collector needed.

Q: What is the difference between String and &str?

String is an owned, heap-allocated, mutable, growable UTF-8 string. &str is a borrowed string slice — a reference to string data stored somewhere (heap, stack, or binary). &str is always immutable. Function parameters should typically accept &str (more flexible — accepts both &String and string literals). Use String when you need to own or modify the string.

Q: What is the difference between Box<T>, Rc<T>, and Arc<T>?

  • Box<T> — single owner, heap allocation; used for recursive types and dynamic dispatch (Box<dyn Trait>)

  • Rc<T> — reference-counted shared ownership, single-threaded only (not Send/Sync)

  • Arc<T> — atomic reference-counted, thread-safe shared ownership (used with Mutex/RwLock for shared mutable state)

Q: What is a lifetime and when do you need to specify one?

Lifetimes describe how long references are valid — they prevent dangling references. Usually inferred by the compiler (lifetime elision). You need explicit lifetime annotations when a function returns a reference and the compiler can't figure out which input reference it comes from: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str.

Q: What is panic! vs Result in Rust?

panic! immediately terminates the thread (and optionally unwinds the stack) — use for unrecoverable errors (bugs, invariant violations). Result<T, E> is for recoverable errors that callers should handle — the idiomatic approach. Never use panic! in library code; always use Result. In application code, unwrap()/expect() are fine in prototypes but should be replaced with proper error handling.

Keep your Rust knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever