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"
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
Create a project:
cargo new hello-rust
cd hello-rust
Run it:
cargo run
You'll get the classic:
Hello, world!
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
I particularly recommend getting comfortable with:
cargo check
It lets you quickly verify whether your code compiles without always producing the final executable.
Then:
cargo clippy
can help identify suspicious or non-idiomatic code.
And:
cargo fmt
keeps formatting consistent.
A simple development loop can therefore become:
cargo fmt
cargo check
cargo clippy
cargo test
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}");
}
Here:
let other_name = name;
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}");
}
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}");
}
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
}
}
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);
}
A type can implement that behavior:
struct Dog;
impl Speak for Dog {
fn speak(&self) {
println!("Woof!");
}
}
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();
}
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}");
}
You can make this more readable with a where clause:
fn print_value<T>(value: T)
where
T: std::fmt::Display,
{
println!("{value}");
}
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,
}
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"),
}
}
This becomes incredibly useful for modeling application state.
For example:
enum ConnectionState {
Connecting,
Connected,
Disconnected,
Failed(String),
}
Instead of passing around strings like:
"connected"
"failed"
"disconnected"
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>
which can contain:
Some(value)
or:
None
For example:
fn find_username(id: u64) -> Option<String> {
if id == 1 {
Some("Charlie".into())
} else {
None
}
}
Then:
match find_username(1) {
Some(name) => println!("User: {name}"),
None => println!("User not found"),
}
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>
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)
}
}
Then:
match divide(10.0, 2.0) {
Ok(result) => println!("{result}"),
Err(error) => println!("Error: {error}"),
}
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),
}
Rust gives you a much cleaner option:
let config = load_config()?;
You can chain operations:
fn start_application() -> Result<(), AppError> {
let config = load_config()?;
let database = connect_database(&config)?;
start_server(database)?;
Ok(())
}
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);
}
}
you can write:
let result: Vec<_> = numbers
.into_iter()
.filter(|n| n % 2 == 0)
.map(|n| n * 2)
.collect();
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();
}
For shared state, you might encounter:
Arc<Mutex<T>>
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());
}
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<_>>
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
}
and:
let result = fetch_data().await;
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
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
and later explore:
Future
↓
Poll
↓
Waker
↓
Executor
when you need deeper control.
Async Closures
One of the notable Rust 2024 additions is support for async closures:
let fetch = async || {
// async work
};
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
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>
For example:
let value = Box::new(42);
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);
For shared ownership across threads:
use std::sync::Arc;
let value = Arc::new(String::from("hello"));
let copy = Arc::clone(&value);
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
}
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();
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) {
// ...
}
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();
But production code needs more thought.
Instead:
let value = operation()?;
or:
match operation() {
Ok(value) => process(value),
Err(error) => handle_error(error),
}
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);
}
}
Run:
cargo test
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
and:
cargo clippy
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
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
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/
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
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
Level 2 — Type System
Then:
Generics
Traits
Trait bounds
Lifetimes
Option
Result
Error handling
Level 3 — Idiomatic Rust
Then:
Iterators
Closures
Modules
Smart pointers
Testing
Documentation
Cargo
Clippy
Rustfmt
Level 4 — Advanced Rust
Then:
Concurrency
Async/await
Future
Send
Sync
Pin
Channels
Macros
Unsafe Rust
FFI
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:?}");
}
}
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
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)