DEV Community

Cover image for Rust Looks Scary. It's Not. You're Just Learning It Wrong.
Nishant Gaurav
Nishant Gaurav

Posted on

Rust Looks Scary. It's Not. You're Just Learning It Wrong.

JavaScript runs your frontend.

Node.js handles your backend.

But then you hit a wall.

Your app slows down. Memory keeps climbing. You Google "why is my Node server eating 2GB of RAM" and some Reddit thread just says — use Rust.

So you look it up. And immediately see:

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

Tab closed.

I don't blame you. But here's the thing — that's advanced Rust. Nobody starts there. Let's start where you should.


What Rust Actually Does

Rust is a systems programming language built for two things that rarely come together: speed and safety.

Think operating systems, game engines, databases, web servers at scale. The kind of software where a crash isn't just a bug report — it's a disaster.

C and C++ owned this space for decades. They're fast. But dangerous. Memory leaks, data races, crashes that appear months later. Rust eliminates entire categories of bugs at compile time. Without a garbage collector. Without a runtime.

That's why Rust has been the most loved language on Stack Overflow for nine years straight. Not most used. Most loved. That's a different metric entirely.


Setting Up

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Enter fullscreen mode Exit fullscreen mode

This installs:

  • rustup — version manager
  • rustc — the compiler
  • cargo — build tool + package manager (think npm, but for Rust)

Create your first project:

cargo new hello_rust
cd hello_rust
cargo run
Enter fullscreen mode Exit fullscreen mode

You'll see Hello, world!

You're in. Let's go.


Variables & Mutability

In most languages, variables are mutable by default. Rust flips this.

fn main() {
    let name = "Luffy";     // immutable by default
    let mut age = 19;       // mut = can be changed later

    // name = "Zoro";       // ❌ ERROR: cannot assign twice to immutable variable
    age = 20;               // ✅ fine

    println!("Name: {}, Age: {}", name, age);
}
Enter fullscreen mode Exit fullscreen mode

This feels restrictive at first. But most bugs come from changing something you didn't mean to change. Rust makes you declare intent upfront.

For values that should never change anywhere:

const MAX_PLAYERS: u32 = 100; // type annotation always required
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: Start with let. Add mut only when you genuinely need it.


Data Types

Rust is statically typed — every value has a type, and the compiler knows all of them before your code even runs.

fn main() {
    // Integers
    let score: i32 = 100;       // signed 32-bit
    let health: u8 = 255;       // unsigned 8-bit (0–255)

    // Floats
    let price: f64 = 29.99;

    // Boolean
    let is_active: bool = true;

    // Character
    let grade: char = 'A';

    // String types (two different things in Rust)
    let language: &str = "Rust";               // string slice (borrowed)
    let greeting: String = String::from("Hi"); // owned String

    println!("{} {} {} {}", score, price, is_active, language);
}
Enter fullscreen mode Exit fullscreen mode

Most of the time, Rust infers types for you:

let x = 42;     // i32
let y = 3.14;   // f64
let z = true;   // bool
Enter fullscreen mode Exit fullscreen mode

Functions

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

// Returns a value — note the arrow
fn add(a: i32, b: i32) -> i32 {
    a + b   // No semicolon = this IS the return value
}

fn main() {
    greet("Zoro");
    println!("5 + 3 = {}", add(5, 3)); // 8
}
Enter fullscreen mode Exit fullscreen mode

The "no semicolon = return value" thing trips everyone up early. Once it clicks, it actually feels clean. Add a semicolon and the expression becomes a statement — returns nothing.


Control Flow

fn main() {
    let score = 87;

    if score >= 90 {
        println!("Grade: A");
    } else if score >= 80 {
        println!("Grade: B");
    } else {
        println!("Grade: F");
    }

    // if as an expression — this is genuinely useful
    let status = if score >= 60 { "Pass" } else { "Fail" };
    println!("Status: {}", status);
}
Enter fullscreen mode Exit fullscreen mode

Loops

fn main() {
    // loop — runs until you break
    let mut count = 0;
    loop {
        count += 1;
        if count == 5 { break; }
    }

    // while
    let mut i = 0;
    while i < 5 {
        println!("{}", i);
        i += 1;
    }

    // for — the one you'll use most
    for number in 1..=5 { // 1..=5 = 1 through 5 inclusive
        println!("{}", number);
    }

    // iterating collections
    let fruits = ["apple", "banana", "mango"];
    for fruit in fruits {
        println!("{}", fruit);
    }
}
Enter fullscreen mode Exit fullscreen mode

Structs — Rust's Version of Objects

struct Player {
    name: String,
    health: u32,
    level: u8,
}

impl Player {
    fn new(name: &str) -> Player {
        Player {
            name: String::from(name),
            health: 100,
            level: 1,
        }
    }

    fn take_damage(&mut self, damage: u32) {
        self.health = self.health.saturating_sub(damage); // never goes below 0
    }

    fn is_alive(&self) -> bool {
        self.health > 0
    }

    fn display(&self) {
        println!("{} | HP: {} | Level: {}", self.name, self.health, self.level);
    }
}

fn main() {
    let mut player = Player::new("Luffy");
    player.display();          // Luffy | HP: 100 | Level: 1

    player.take_damage(30);
    player.display();          // Luffy | HP: 70 | Level: 1

    player.take_damage(200);
    println!("Alive? {}", player.is_alive()); // false
}
Enter fullscreen mode Exit fullscreen mode

&self = reading the struct. &mut self = modifying it. This pattern shows up everywhere in Rust.


Enums — More Powerful Than You Think

Rust enums can hold data. This makes them genuinely different from enums in other languages.

enum Shape {
    Circle(f64),          // radius
    Rectangle(f64, f64),  // width, height
}

fn area(shape: Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
    }
}

fn main() {
    println!("Circle: {:.2}", area(Shape::Circle(5.0)));             // 78.54
    println!("Rectangle: {:.2}", area(Shape::Rectangle(4.0, 6.0))); // 24.00
}
Enter fullscreen mode Exit fullscreen mode

That match block is exhaustive. Forget a case → compiler error. Not a runtime panic. Not a wrong result. A compile-time error. Before your code runs.


The Big Three: Ownership, Borrowing, References

This is what people mean when they say "Rust is hard."

It's not impossible. It's just a different mental model. Give it real attention here and everything else in Rust becomes much clearer.

Ownership

Every value in Rust has exactly one owner. When the owner is gone, the value is dropped (memory freed). Automatically. No garbage collector.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // ownership MOVES to s2

    // println!("{}", s1); // ❌ ERROR: s1 no longer valid
    println!("{}", s2);    // ✅ fine
}
Enter fullscreen mode Exit fullscreen mode

In JavaScript, both variables would point to the same string. In Rust, ownership transfers. s1 is gone.

Why? If two variables could own the same memory, who frees it? Double-free = crash. Rust makes this impossible — at compile time.

Borrowing

What if you need to use a value without taking ownership? You borrow it with &.

fn print_length(s: &String) { // borrow, don't own
    println!("Length: {}", s.len());
} // s goes out of scope, nothing freed — we didn't own it

fn main() {
    let s1 = String::from("hello world");
    print_length(&s1);  // lend s1
    println!("{}", s1); // s1 still valid
}
Enter fullscreen mode Exit fullscreen mode

Mutable References

fn add_exclamation(s: &mut String) {
    s.push_str("!!!");
}

fn main() {
    let mut message = String::from("Hello");
    add_exclamation(&mut message);
    println!("{}", message); // Hello!!!
}
Enter fullscreen mode Exit fullscreen mode

The rules Rust enforces:

  • Unlimited read-only references (&T) — all at once ✅
  • Exactly ONE mutable reference (&mut T) — no other references simultaneously ✅
  • Both at the same time — ❌ never

This eliminates an entire class of data race bugs. At compile time.


Option — No More Null

Rust has no null. Instead, there's Option<T>:

fn find_player(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Luffy"))
    } else {
        None
    }
}

fn main() {
    // Pattern match
    match find_player(1) {
        Some(name) => println!("Found: {}", name),
        None => println!("Not found"),
    }

    // Shorthand with default
    let player = find_player(99).unwrap_or(String::from("Unknown"));
    println!("{}", player); // Unknown

    // if let — when you only care about Some
    if let Some(name) = find_player(1) {
        println!("Welcome, {}!", name);
    }
}
Enter fullscreen mode Exit fullscreen mode

The key: Rust forces you to handle None. You cannot accidentally call methods on a missing value. The compiler won't allow it.


Result — Handling Errors Properly

For operations that can fail:

fn parse_age(input: &str) -> Result<u32, String> {
    match input.trim().parse::<u32>() {
        Ok(n) => Ok(n),
        Err(_) => Err(format!("'{}' is not a valid age", input)),
    }
}

fn main() {
    match parse_age("25") {
        Ok(age) => println!("Valid age: {}", age),
        Err(e) => println!("Error: {}", e),
    }

    match parse_age("abc") {
        Ok(age) => println!("Valid age: {}", age),
        Err(e) => println!("Error: {}", e), // this one runs
    }
}
Enter fullscreen mode Exit fullscreen mode

The ? operator propagates errors automatically — you don't rewrite the same match block everywhere.


Vectors & HashMaps

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    numbers.push(6);

    // Safe access — no crash on out-of-bounds
    println!("{:?}", numbers.get(10)); // None

    // Just like JS array methods
    let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();

    println!("{:?}", doubled); // [2, 4, 6, 8, 10, 12]
    println!("{:?}", evens);   // [2, 4, 6]
}
Enter fullscreen mode Exit fullscreen mode
use std::collections::HashMap;

fn main() {
    let mut scores: HashMap<String, u32> = HashMap::new();

    scores.insert(String::from("Luffy"), 95);
    scores.insert(String::from("Zoro"), 88);

    // Insert only if key doesn't exist
    scores.entry(String::from("Nami")).or_insert(91);

    if let Some(s) = scores.get("Luffy") {
        println!("Luffy: {}", s); // 95
    }

    for (name, score) in &scores {
        println!("{}: {}", name, score);
    }
}
Enter fullscreen mode Exit fullscreen mode

Mini Project — CLI Todo App

Let's put everything together:

use std::collections::HashMap;
use std::io;

struct TodoApp {
    tasks: HashMap<u32, (String, bool)>,
    next_id: u32,
}

impl TodoApp {
    fn new() -> TodoApp {
        TodoApp { tasks: HashMap::new(), next_id: 1 }
    }

    fn add(&mut self, desc: &str) {
        self.tasks.insert(self.next_id, (desc.to_string(), false));
        println!("✅ Task #{} added: {}", self.next_id, desc);
        self.next_id += 1;
    }

    fn complete(&mut self, id: u32) {
        match self.tasks.get_mut(&id) {
            Some(task) => { task.1 = true; println!("🎉 Task #{} done!", id); }
            None => println!("❌ Task #{} not found.", id),
        }
    }

    fn list(&self) {
        if self.tasks.is_empty() {
            println!("No tasks yet."); return;
        }
        let mut ids: Vec<&u32> = self.tasks.keys().collect();
        ids.sort();
        println!("\n--- Tasks ---");
        for id in ids {
            let (desc, done) = &self.tasks[id];
            let icon = if *done { "✅" } else { "⏳" };
            println!("{} #{}: {}", icon, id, desc);
        }
        println!("-------------\n");
    }
}

fn main() {
    let mut app = TodoApp::new();
    println!("=== Rust Todo ===");
    println!("Commands: add <task> | done <id> | list | quit\n");

    loop {
        let mut input = String::new();
        io::stdin().read_line(&mut input).unwrap();
        let input = input.trim();

        if input.starts_with("add ") {
            app.add(&input[4..]);
        } else if input.starts_with("done ") {
            match input[5..].parse::<u32>() {
                Ok(id) => app.complete(id),
                Err(_) => println!("Enter a valid number"),
            }
        } else if input == "list" {
            app.list();
        } else if input == "quit" {
            println!("Goodbye! 🦀"); break;
        } else {
            println!("Unknown command.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Run with cargo run. Fully working CLI app — structs, methods, HashMap, Option, pattern matching, and user input. Everything from this guide in one place.


The Compiler Is Not Your Enemy

Rust's error messages are some of the best in any language:

error[E0382]: borrow of moved value: `s1`
  --> src/main.rs:5:20
   |
3  |     let s1 = String::from("hello");
   |         -- move occurs because `s1` has type `String`
4  |     let s2 = s1;
   |              -- value moved here
5  |     println!("{}", s1);
   |                    ^^ value borrowed here after move
Enter fullscreen mode Exit fullscreen mode

This error saved you from a bug. The compiler caught it before your code ever ran.

Read every error. The whole thing.


Common Mistakes to Avoid

Fighting the borrow checker. If it's complaining, your design needs adjusting — not the rules.

Cloning everything. clone() works, but it usually means you haven't understood ownership yet. Understand first, then decide.

Jumping to async Rust too early. Get the basics solid first.

Skimming error messages. They're verbose for a reason. Read them fully.

Comparing everything to JavaScript. Rust solves different problems with a different mental model. Let it be different.


How to Practice

Build small, real things:

  • Number guessing game (stdin, random, loops)
  • Temperature converter (functions, structs)
  • Word counter for a text file (file I/O, HashMap)
  • Student grade tracker (Vec, structs, sorting)
  • Simple calculator (enums, pattern matching)

Use cargo check instead of cargo build while iterating — gives you errors much faster.

The Rust Book is free, excellent, and written by people who care about making this approachable.

Progress in Rust feels slow, and then suddenly clicks. You go from fighting the compiler to being grateful for it. That shift happens. Give it time.


What's Next?

You now have the foundation:

Variables. Functions. Structs. Enums. Ownership. Option and Result. Vectors. HashMaps.

That's enough to build real things.

Next in this series → Building web servers and APIs with Axum + Tokio, Rust's async runtime. Fast, clean, safe backend code.


Happy building. 🦀

Top comments (0)