Bookmark this page. Use Ctrl+F (or Cmd+F on Mac) to find what you need. This cheat sheet covers Rust syntax, ownership, traits, error handling, and common patterns. Try examples at play.rust-lang.org.

Last updated: March 2026

Variables and Types

let name = "Alex";            // immutable (default)
let mut count = 0;            // mutable
let age: i32 = 25;            // explicit type
const MAX: u32 = 100;         // compile-time constant
TypeDescriptionExample
i8, i16, i32, i64, i128Signed integerslet x: i32 = -42;
u8, u16, u32, u64, u128Unsigned integerslet x: u32 = 42;
f32, f64Floating pointlet x: f64 = 3.14;
boolBooleantrue, false
charUnicode character'A', '🚀'
&strString slice (borrowed)"hello"
StringOwned string (heap)String::from("hello")
()Unit type (void)fn do_thing() { }
[T; N]Fixed array[1, 2, 3]
Vec<T>Dynamic arrayvec![1, 2, 3]
(T, U)Tuple(42, "hello")
Option<T>Nullable valueSome(42) or None
Result<T, E>Success or errorOk(42) or Err("fail")

Type Conversions

let x: i32 = 42;
let y: f64 = x as f64;           // 42.0
let s: String = x.to_string();   // "42"
let n: i32 = "42".parse().unwrap(); // 42
let n: i32 = "42".parse().unwrap_or(0); // 42 (or 0 on error)

Strings

// &str — string slice, borrowed, immutable
let greeting: &str = "hello";

// String — owned, heap-allocated, growable
let mut name = String::from("Alex");
name.push_str(" Smith");        // append
name.push('!');                 // append char

// Conversions
let s: String = "hello".to_string();
let s: &str = &name;            // String → &str (auto-deref)

// Common methods
name.len()                      // byte length
name.is_empty()                 // true if empty
name.contains("Alex")           // substring check
name.starts_with("A")
name.to_uppercase()
name.to_lowercase()
name.trim()                     // remove whitespace
name.replace("Alex", "Sam")
name.split(" ")                 // iterator of parts

// Format
let msg = format!("Hello {name}, age {age}");

Ownership and Borrowing

// Ownership — each value has one owner
let s1 = String::from("hello");
let s2 = s1;                    // s1 is MOVED to s2
// println!("{s1}");             // ERROR: s1 no longer valid

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

// Borrowing — reference without taking ownership
fn print_len(s: &String) {      // immutable borrow
    println!("{}", s.len());
}

fn add_excl(s: &mut String) {   // mutable borrow
    s.push('!');
}

// Rules:
// 1. Many immutable references (&T) OR one mutable reference (&mut T)
// 2. References must always be valid (no dangling)

Structs

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

let user = User {
    name: String::from("Alex"),
    age: 25,
    active: true,
};

// Access
println!("{}", user.name);

// Update syntax
let user2 = User { age: 26, ..user };

// Tuple struct
struct Point(f64, f64);
let p = Point(1.0, 2.0);

// Methods
impl User {
    // Constructor (convention)
    fn new(name: &str, age: u32) -> Self {
        Self { name: name.to_string(), age, active: true }
    }

    // Method (takes &self)
    fn greet(&self) -> String {
        format!("Hi, I'm {}", self.name)
    }
}

Enums and Pattern Matching

enum Direction { North, South, East, West }

// Enums with data
enum Shape {
    Circle(f64),                     // radius
    Rectangle(f64, f64),             // width, height
    Triangle { base: f64, height: f64 },
}

// Pattern matching with match
match shape {
    Shape::Circle(r) => std::f64::consts::PI * r * r,
    Shape::Rectangle(w, h) => w * h,
    Shape::Triangle { base, height } => 0.5 * base * height,
}

// if let — match a single pattern
if let Some(value) = optional {
    println!("Got: {value}");
}

// let else — match or diverge
let Some(value) = optional else {
    return;
};

Option and Result

// Option<T> — value or nothing
let name: Option<&str> = Some("Alex");
let empty: Option<&str> = None;

name.unwrap()              // "Alex" (panics if None)
name.unwrap_or("Unknown")  // "Alex" (or default)
name.is_some()             // true
name.is_none()             // false
name.map(|n| n.len())      // Some(4)

// Result<T, E> — success or error
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

// ? operator — propagate errors
fn read_file(path: &str) -> Result<String, std::io::Error> {
    let content = std::fs::read_to_string(path)?;  // returns Err early
    Ok(content)
}

// Handle Result
match divide(10.0, 3.0) {
    Ok(result) => println!("{result}"),
    Err(e) => println!("Error: {e}"),
}

Traits

// Define a trait
trait Greet {
    fn greet(&self) -> String;

    // Default implementation
    fn hello(&self) -> String {
        format!("Hello from {}", self.greet())
    }
}

// Implement for a type
impl Greet for User {
    fn greet(&self) -> String {
        self.name.clone()
    }
}

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

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

// Common derive traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point { x: i32, y: i32 }

Collections

// Vec — dynamic array
let mut v = vec![1, 2, 3];
v.push(4);
v.pop();                    // Some(4)
v.len();                    // 3
v[0];                       // 1 (panics if out of bounds)
v.get(0);                   // Some(&1) (safe)
v.contains(&2);             // true
v.iter().filter(|&&x| x > 1).collect::<Vec<_>>();

// HashMap
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("name", "Alex");
map.get("name");             // Some(&"Alex")
map.contains_key("name");    // true
map.entry("age").or_insert("25");
for (key, value) in &map { }

// HashSet
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(1);
set.contains(&1);           // true

Iterators

let nums = vec![1, 2, 3, 4, 5];

nums.iter().map(|x| x * 2).collect::<Vec<_>>();    // [2, 4, 6, 8, 10]
nums.iter().filter(|&&x| x > 2).collect::<Vec<_>>(); // [3, 4, 5]
nums.iter().sum::<i32>();                            // 15
nums.iter().any(|&x| x > 3);                        // true
nums.iter().all(|&x| x > 0);                        // true
nums.iter().find(|&&x| x > 3);                      // Some(&4)
nums.iter().position(|&x| x == 3);                  // Some(2)
nums.iter().enumerate();                             // (index, &value)
nums.iter().zip(other.iter());                       // pair elements
nums.iter().take(3).collect::<Vec<_>>();              // [1, 2, 3]
nums.iter().skip(2).collect::<Vec<_>>();              // [3, 4, 5]
nums.iter().flat_map(|x| vec![x, x * 10]);
nums.iter().fold(0, |acc, &x| acc + x);             // 15

Control Flow

// if / else (is an expression)
let status = if age >= 18 { "adult" } else { "minor" };

// loop (infinite, break with value)
let result = loop {
    if condition { break 42; }
};

// while
while count > 0 { count -= 1; }

// for
for i in 0..5 { }           // 0, 1, 2, 3, 4
for i in 0..=5 { }          // 0, 1, 2, 3, 4, 5
for item in &vec { }        // iterate by reference
for item in vec { }          // iterate by value (moves)

Closures

let add = |a: i32, b: i32| -> i32 { a + b };
let double = |x| x * 2;     // types inferred
let greet = || println!("Hello");

// Closures capture variables
let name = String::from("Alex");
let greet = || println!("Hello {name}");   // borrows name
let greet = move || println!("Hello {name}"); // takes ownership

Cargo Commands

CommandDescription
cargo new my_appCreate a new project
cargo runBuild and run
cargo buildBuild (debug)
cargo build --releaseBuild (optimized)
cargo testRun tests
cargo checkFast compile check (no binary)
cargo clippyLint your code
cargo fmtAuto-format code
cargo add serdeAdd a dependency
cargo doc --openGenerate and open docs

Lifetimes

// Lifetimes tell the compiler how long references are valid
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

// Struct with a reference needs a lifetime
struct Excerpt<'a> {
    text: &'a str,
}

Rule: if a function returns a reference, it must come from one of the inputs (annotated with the same lifetime).

Async / Await

// async functions return a Future
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    let body = reqwest::get(url).await?.text().await?;
    Ok(body)
}

// Use tokio runtime
#[tokio::main]
async fn main() {
    let data = fetch_data("https://api.example.com").await.unwrap();
    println!("{data}");
}

// Run tasks in parallel
let (a, b) = tokio::join!(fetch_a(), fetch_b());

Common Mistakes

  1. Fighting the borrow checker — If you are cloning everything to make the compiler happy, step back. Usually there is a better way to structure ownership. Use references (&T, &mut T) first, clone only when needed.

  2. unwrap() in productionunwrap() panics on None or Err. Use ?, unwrap_or(), unwrap_or_default(), or proper match/if let in production code.

  3. Returning references to local variables — A function cannot return &String if the String was created inside the function. The reference would outlive the data. Return String (owned) instead.