DEV Community

Cover image for Modern Rust in 2026: What Developers Should Actually Learn
Ajnas ahammed
Ajnas ahammed

Posted on

Modern Rust in 2026: What Developers Should Actually Learn

Rust has been around for years, but it still feels surprisingly modern.

It gives you low-level control without forcing you to manually manage memory in the same way you would in C or C++. At the same time, it provides powerful abstractions, expressive types, modern tooling, and a compiler that catches a huge class of bugs before your program runs.

If you're learning Rust in 2026, though, I don't think the best approach is to simply memorize the syntax.

You need to understand how modern Rust thinks.

That means ownership, borrowing, traits, error handling, iterators, concurrency, async programming, Cargo, testing, and good project structure.

Let's explore it.


Why Rust?

Rust is designed around a pretty interesting combination:

  • Performance
  • Memory safety
  • Concurrency
  • Strong typing
  • Zero-cost abstractions
  • Excellent tooling
  • Native compilation

The interesting part is that Rust doesn't use a garbage collector to provide its memory-safety guarantees.

Instead, ownership and borrowing are checked by the compiler.

That sounds intimidating at first.

It is.

But once ownership starts making sense, Rust becomes much easier to reason about.


Rust in 2026: Start With Rust 2024

If you're starting a new project today, Rust 2024 should be part of your baseline knowledge.

Rust 2024 became stable with Rust 1.85.0 in February 2025. It introduced several language and edition changes, including updated lifetime-capture rules, changes around temporary scopes, safer extern blocks, and support for async closures.

The current Rust Book assumes Rust 1.90 or later with:

[package]
name = "my-project"
version = "0.1.0"
edition = "2024"
Enter fullscreen mode Exit fullscreen mode

The important thing is that an edition isn't simply a completely new language version.

Rust editions provide a way to introduce changes while maintaining compatibility across the ecosystem.


Getting Started

If you don't have Rust installed, rustup is the standard toolchain manager.

Check your installation:

rustc --version
cargo --version
rustup --version
Enter fullscreen mode Exit fullscreen mode

Create a project:

cargo new hello-rust
cd hello-rust
Enter fullscreen mode Exit fullscreen mode

Run it:

cargo run
Enter fullscreen mode Exit fullscreen mode

You'll get the classic:

Hello, world!
Enter fullscreen mode Exit fullscreen mode

But Cargo is much more than a command that runs your application.

It becomes the center of your Rust workflow.


Learn Cargo Early

These commands are worth memorizing:

cargo check
cargo build
cargo run
cargo test
cargo fmt
cargo clippy
Enter fullscreen mode Exit fullscreen mode

I particularly recommend getting comfortable with:

cargo check
Enter fullscreen mode Exit fullscreen mode

It lets you quickly verify whether your code compiles without always producing the final executable.

Then:

cargo clippy
Enter fullscreen mode Exit fullscreen mode

can help identify suspicious or non-idiomatic code.

And:

cargo fmt
Enter fullscreen mode Exit fullscreen mode

keeps formatting consistent.

A simple development loop can therefore become:

cargo fmt
cargo check
cargo clippy
cargo test
Enter fullscreen mode Exit fullscreen mode

This is one of the reasons Rust's tooling feels so cohesive.


Ownership Is the Big One

If you're coming from JavaScript, Python, Java, or another garbage-collected language, ownership is probably going to be the biggest mental shift.

Consider:

fn main() {
    let name = String::from("Charlie");

    let other_name = name;

    println!("{other_name}");
}
Enter fullscreen mode Exit fullscreen mode

Here:

let other_name = name;
Enter fullscreen mode Exit fullscreen mode

moves ownership of the String.

The original name can no longer be used as if it still owned the value.

Why?

Because Rust needs to know exactly who is responsible for the data.

This ownership model is one of the foundations of Rust's memory-safety guarantees.


Borrow Instead of Moving

You don't always need to transfer ownership.

You can borrow:

fn print_name(name: &str) {
    println!("{name}");
}

fn main() {
    let name = String::from("Charlie");

    print_name(&name);

    println!("{name}");
}
Enter fullscreen mode Exit fullscreen mode

The function receives a reference rather than taking ownership.

This is where Rust starts becoming interesting.

Instead of relying on a garbage collector to figure out memory usage later, the compiler checks the ownership relationships while you're writing the program.


Mutable Borrowing

Rust also controls mutable access.

fn add_exclamation(message: &mut String) {
    message.push('!');
}

fn main() {
    let mut message = String::from("Hello");

    add_exclamation(&mut message);

    println!("{message}");
}
Enter fullscreen mode Exit fullscreen mode

The &mut tells Rust that the function is borrowing the value mutably.

Rust's borrowing rules prevent conflicting accesses that could otherwise result in memory bugs.


Don't Fear Lifetimes

Eventually you'll encounter something like:

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

This is a lifetime annotation.

The important thing is not to start by memorizing every lifetime rule.

Think of a lifetime as describing the relationship between references.

Rust uses this information to verify that references don't outlive the data they're referring to.

And in many situations, Rust can infer lifetimes for you.


Traits Are One of Rust's Superpowers

If you're coming from object-oriented programming, Rust's traits may initially feel different.

A trait defines behavior.

trait Speak {
    fn speak(&self);
}
Enter fullscreen mode Exit fullscreen mode

A type can implement that behavior:

struct Dog;

impl Speak for Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can use the trait as an abstraction.

Traits become even more powerful when combined with generics:

fn make_it_speak<T: Speak>(animal: &T) {
    animal.speak();
}
Enter fullscreen mode Exit fullscreen mode

This is one of the areas where Rust's type system becomes extremely expressive.


Generics Without Giving Up Performance

Rust lets you write generic code without necessarily paying the runtime cost associated with dynamic dispatch.

For example:

fn print_value<T: std::fmt::Display>(value: T) {
    println!("{value}");
}
Enter fullscreen mode Exit fullscreen mode

You can make this more readable with a where clause:

fn print_value<T>(value: T)
where
    T: std::fmt::Display,
{
    println!("{value}");
}
Enter fullscreen mode Exit fullscreen mode

As your trait bounds become more complicated, where clauses can make APIs much easier to understand.


Enums Are Much More Than Constants

Rust enums can contain data.

enum Message {
    Text(String),
    Number(i32),
    Quit,
}
Enter fullscreen mode Exit fullscreen mode

Then you can use pattern matching:

fn handle(message: Message) {
    match message {
        Message::Text(text) => println!("Text: {text}"),
        Message::Number(number) => println!("Number: {number}"),
        Message::Quit => println!("Quit"),
    }
}
Enter fullscreen mode Exit fullscreen mode

This becomes incredibly useful for modeling application state.

For example:

enum ConnectionState {
    Connecting,
    Connected,
    Disconnected,
    Failed(String),
}
Enter fullscreen mode Exit fullscreen mode

Instead of passing around strings like:

"connected"
"failed"
"disconnected"
Enter fullscreen mode Exit fullscreen mode

you can make the possible states explicit in the type system.


Option<T> Instead of Null

Rust doesn't use null as its standard mechanism for missing values.

Instead, you get:

Option<T>
Enter fullscreen mode Exit fullscreen mode

which can contain:

Some(value)
Enter fullscreen mode Exit fullscreen mode

or:

None
Enter fullscreen mode Exit fullscreen mode

For example:

fn find_username(id: u64) -> Option<String> {
    if id == 1 {
        Some("Charlie".into())
    } else {
        None
    }
}
Enter fullscreen mode Exit fullscreen mode

Then:

match find_username(1) {
    Some(name) => println!("User: {name}"),
    None => println!("User not found"),
}
Enter fullscreen mode Exit fullscreen mode

The compiler makes the possibility of "no value" explicit.


Result<T, E> for Errors

Rust also makes errors explicit.

Instead of silently returning a special value, a function can return:

Result<T, E>
Enter fullscreen mode Exit fullscreen mode

For example:

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Cannot divide by zero".into())
    } else {
        Ok(a / b)
    }
}
Enter fullscreen mode Exit fullscreen mode

Then:

match divide(10.0, 2.0) {
    Ok(result) => println!("{result}"),
    Err(error) => println!("Error: {error}"),
}
Enter fullscreen mode Exit fullscreen mode

This is one of the patterns you'll use constantly in real Rust applications.


The ? Operator Makes Error Handling Better

Imagine having to manually propagate every error:

match load_config() {
    Ok(config) => config,
    Err(error) => return Err(error),
}
Enter fullscreen mode Exit fullscreen mode

Rust gives you a much cleaner option:

let config = load_config()?;
Enter fullscreen mode Exit fullscreen mode

You can chain operations:

fn start_application() -> Result<(), AppError> {
    let config = load_config()?;
    let database = connect_database(&config)?;
    start_server(database)?;

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

This style keeps the happy path readable while still propagating errors correctly.


Iterators Are Worth Learning

Rust's iterator system is one of the places where the language feels very modern.

Instead of:

let mut result = Vec::new();

for number in numbers {
    if number % 2 == 0 {
        result.push(number * 2);
    }
}
Enter fullscreen mode Exit fullscreen mode

you can write:

let result: Vec<_> = numbers
    .into_iter()
    .filter(|n| n % 2 == 0)
    .map(|n| n * 2)
    .collect();
Enter fullscreen mode Exit fullscreen mode

This style can make data transformations much easier to read.

And because iterators are lazy, operations such as filter and map can form a pipeline rather than immediately creating intermediate collections.


Concurrency Without Garbage-Collector Magic

Rust's ownership and type systems also play a major role in concurrent programming.

A basic thread:

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        println!("Hello from another thread!");
    });

    handle.join().unwrap();
}
Enter fullscreen mode Exit fullscreen mode

For shared state, you might encounter:

Arc<Mutex<T>>
Enter fullscreen mode Exit fullscreen mode

For example:

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

fn main() {
    let counter = Arc::new(Mutex::new(0));

    let mut handles = Vec::new();

    for _ in 0..4 {
        let counter = Arc::clone(&counter);

        handles.push(thread::spawn(move || {
            let mut value = counter.lock().unwrap();
            *value += 1;
        }));
    }

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

    println!("{}", *counter.lock().unwrap());
}
Enter fullscreen mode Exit fullscreen mode

Rust's ownership and type systems are designed to prevent many classes of concurrency bugs at compile time.


But Don't Reach for Arc<Mutex<T>> Immediately

This is an important practical lesson.

When the compiler makes shared state difficult, your first response shouldn't always be:

Arc<Mutex<_>>
Enter fullscreen mode Exit fullscreen mode

Ask:

Who should own this data?

Then:

Who actually needs access to it?

Then:

Does that access really need to be mutable?

Sometimes a channel or a better ownership design is much cleaner than shared mutable state.

Rust often pushes you toward better architecture.


Async Rust

Async Rust is a major part of modern Rust development.

The basic syntax looks familiar:

async fn fetch_data() {
    // async work
}
Enter fullscreen mode Exit fullscreen mode

and:

let result = fetch_data().await;
Enter fullscreen mode Exit fullscreen mode

But there's an important detail:

Rust does not ship with one built-in async runtime.

Async functions produce futures, and an async runtime is responsible for executing those futures.

The Rust Book explains that async/await builds on the Future trait and that async code needs a runtime to actually execute those futures.

This is why ecosystems such as Tokio are so important in practical async Rust applications.


Futures, Pin, and Poll

If you're going deeper into async Rust, you'll eventually encounter:

Future
Pin
Poll
Waker
Enter fullscreen mode Exit fullscreen mode

At first, this can look terrifying.

You don't need to understand every internal detail before building an async application.

But understanding the basic model helps.

An async function is transformed into a future representing work that may be suspended and resumed. Rust's compiler and async machinery maintain the ownership and borrowing guarantees across those suspension points.

You can start with:

async/await
Enter fullscreen mode Exit fullscreen mode

and later explore:

Future
   ↓
Poll
   ↓
Waker
   ↓
Executor
Enter fullscreen mode Exit fullscreen mode

when you need deeper control.


Async Closures

One of the notable Rust 2024 additions is support for async closures:

let fetch = async || {
    // async work
};
Enter fullscreen mode Exit fullscreen mode

Rust 1.85 stabilized async closures alongside the 2024 Edition.

This is useful when you need closures that themselves perform asynchronous operations.


Send and Sync

When working with threads and async runtimes, two traits appear frequently:

Send
Sync
Enter fullscreen mode Exit fullscreen mode

A simplified way to think about them is:

Send: a value can be transferred between threads safely.

Sync: references to a value can be shared between threads safely.

You don't usually need to implement these traits manually.

Instead, Rust determines whether your types satisfy the requirements based on the types they contain.

Once you start building multi-threaded or async applications, understanding these two traits becomes increasingly important.


Smart Pointers

Rust provides several smart-pointer types.

Some important ones are:

Box<T>
Rc<T>
Arc<T>
Cell<T>
RefCell<T>
Mutex<T>
RwLock<T>
Enter fullscreen mode Exit fullscreen mode

For example:

let value = Box::new(42);
Enter fullscreen mode Exit fullscreen mode

For shared ownership in single-threaded code:

use std::rc::Rc;

let value = Rc::new(String::from("hello"));

let a = Rc::clone(&value);
let b = Rc::clone(&value);
Enter fullscreen mode Exit fullscreen mode

For shared ownership across threads:

use std::sync::Arc;

let value = Arc::new(String::from("hello"));

let copy = Arc::clone(&value);
Enter fullscreen mode Exit fullscreen mode

The important part isn't memorizing all of these.

It's understanding why you need one.


Unsafe Rust

Rust isn't completely "unsafe-free."

It gives you an explicit boundary:

unsafe {
    // unsafe operations
}
Enter fullscreen mode Exit fullscreen mode

Unsafe Rust may be necessary for:

  • FFI
  • Hardware access
  • Operating-system interfaces
  • Raw pointers
  • Low-level optimizations
  • Custom memory management

The goal isn't to eliminate unsafe code entirely.

The goal is to keep unsafe code contained and build safe abstractions around it.


Don't Use clone() to Silence the Compiler

This is probably one of the most useful lessons for beginners.

You write:

let copy = value.clone();
Enter fullscreen mode Exit fullscreen mode

and suddenly the compiler stops complaining.

Problem solved?

Maybe.

But maybe you've just hidden an ownership problem.

Sometimes cloning is exactly what you want.

Sometimes you should borrow:

fn process(data: &Data) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Before adding .clone(), ask:

Do I actually need another owned copy?

If the answer is no, a reference may be the better design.


Don't Use unwrap() Everywhere

During experimentation, this is perfectly normal:

let value = operation().unwrap();
Enter fullscreen mode Exit fullscreen mode

But production code needs more thought.

Instead:

let value = operation()?;
Enter fullscreen mode Exit fullscreen mode

or:

match operation() {
    Ok(value) => process(value),
    Err(error) => handle_error(error),
}
Enter fullscreen mode Exit fullscreen mode

unwrap() isn't inherently bad.

It just means:

"I believe this cannot fail here."

Make sure that statement is actually true.


Testing

Rust has built-in testing support.

fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn addition_works() {
        assert_eq!(add(2, 3), 5);
    }
}
Enter fullscreen mode Exit fullscreen mode

Run:

cargo test
Enter fullscreen mode Exit fullscreen mode

You can also put executable examples in documentation, allowing code examples to be checked as part of your testing workflow.

This is particularly useful when developing libraries.


Clippy + Rustfmt

Two tools I recommend using from the beginning:

cargo fmt
Enter fullscreen mode Exit fullscreen mode

and:

cargo clippy
Enter fullscreen mode Exit fullscreen mode

rustfmt handles formatting.

Clippy provides additional lints that can catch common mistakes and encourage idiomatic Rust.

Your CI pipeline can therefore include:

cargo fmt --check
cargo clippy
cargo test
Enter fullscreen mode Exit fullscreen mode

That's a very reasonable baseline for a Rust project.


A Practical Rust Project Structure

For a small project:

my-app/
├── Cargo.toml
└── src/
    └── main.rs
Enter fullscreen mode Exit fullscreen mode

is completely fine.

Don't create a massive architecture before you have a problem to solve.

As the project grows:

my-app/
├── Cargo.toml
├── src/
│   ├── main.rs
│   ├── config.rs
│   ├── error.rs
│   ├── models.rs
│   ├── services.rs
│   └── api/
│       ├── mod.rs
│       └── users.rs
└── tests/
Enter fullscreen mode Exit fullscreen mode

can make sense.

For larger systems, Cargo workspaces can separate applications and libraries into multiple crates.


Rust for Backend Development

Rust is a strong option for backend systems where performance, resource efficiency, and correctness matter.

You can use it for:

  • REST APIs
  • WebSockets
  • Networking
  • Authentication
  • Microservices
  • Database services
  • Infrastructure

A typical architecture might look like:

Client
   |
   v
API
   |
   +---- Authentication
   |
   +---- Business Logic
   |
   +---- Database
   |
   +---- Cache
   |
   +---- External Services
Enter fullscreen mode Exit fullscreen mode

Async Rust becomes especially useful when your application spends significant time waiting on I/O.


Rust for CLI Tools

Rust is also excellent for command-line applications.

You get:

  • Native binaries
  • Fast startup
  • Strong error handling
  • Cross-platform support
  • Excellent dependency management

This makes Rust a particularly interesting choice for developer tooling.


Rust for Embedded Systems

If you're interested in electronics, robotics, or embedded systems, Rust becomes even more interesting.

Rust can be used for:

  • Microcontrollers
  • Sensors
  • Robotics
  • IoT
  • Hardware interfaces
  • Automotive systems
  • Low-level firmware

The combination of low-level control and compile-time safety makes it worth exploring for embedded development.


What Should You Actually Learn?

Don't try to learn everything at once.

I'd approach modern Rust in roughly this order:

Level 1 — Fundamentals

Learn:

Variables
Functions
Structs
Enums
Pattern matching
Collections
Ownership
Borrowing
Enter fullscreen mode Exit fullscreen mode

Level 2 — Type System

Then:

Generics
Traits
Trait bounds
Lifetimes
Option
Result
Error handling
Enter fullscreen mode Exit fullscreen mode

Level 3 — Idiomatic Rust

Then:

Iterators
Closures
Modules
Smart pointers
Testing
Documentation
Cargo
Clippy
Rustfmt
Enter fullscreen mode Exit fullscreen mode

Level 4 — Advanced Rust

Then:

Concurrency
Async/await
Future
Send
Sync
Pin
Channels
Macros
Unsafe Rust
FFI
Enter fullscreen mode Exit fullscreen mode

Level 5 — Build Something

This is the important part.

Build:

  • A CLI tool
  • A REST API
  • A TCP server
  • A file-processing application
  • A database-backed service
  • An embedded project
  • A WebAssembly application

You learn Rust much faster when the compiler is complaining about a real project rather than a tutorial exercise.


A Small Example Combining Modern Rust Concepts

Here's a small example using structs, borrowing, iterators, and filtering:

#[derive(Debug)]
struct User {
    name: String,
    age: u32,
}

fn adult_users(users: &[User]) -> Vec<&User> {
    users
        .iter()
        .filter(|user| user.age >= 18)
        .collect()
}

fn main() {
    let users = vec![
        User {
            name: "Alice".into(),
            age: 20,
        },
        User {
            name: "Bob".into(),
            age: 16,
        },
    ];

    let adults = adult_users(&users);

    for user in adults {
        println!("{user:?}");
    }
}
Enter fullscreen mode Exit fullscreen mode

It's a small program, but look at how many Rust concepts are already involved:

  • Structs
  • Ownership
  • Borrowing
  • Slices
  • References
  • Iterators
  • Closures
  • Type inference

That's why Rust can feel difficult initially.

There are a lot of concepts interacting with each other.

But that's also what makes the language powerful.


My Biggest Rust Tip

Don't try to "beat" the borrow checker.

Work with it.

When Rust gives you an error, don't immediately add:

.clone()
Arc
Mutex
RefCell
unsafe
Enter fullscreen mode Exit fullscreen mode

Instead, stop and ask:

Who owns this data?

Who needs it?

Does that code need to modify it?

How long does the reference actually need to live?

Those questions often lead to a much cleaner solution.


Final Thoughts

Modern Rust isn't just about writing fast code.

It's about making the relationships inside your program explicit.

Who owns the data?

Who can modify it?

What happens when something fails?

Can two threads access this safely?

Can this reference outlive the data?

What should happen when a value doesn't exist?

Rust makes you answer these questions early.

At first, that can feel annoying.

Later, it starts feeling like one of the language's biggest advantages.

If you're starting Rust in 2026, don't focus only on syntax.

Learn the mental model.

Start with ownership and borrowing.

Then move into traits, enums, Result, iterators, concurrency, async Rust, and the tooling around Cargo.

And most importantly:

Build things.

The compiler isn't your enemy.

Let it teach you.


What's Next?

If you're exploring software development alongside Rust, you can also check out my web development work:

Ajnas Ahammed — Web Development

Keep building, keep breaking things, and keep learning.

That's how you get better at programming.

Top comments (0)