Quick answer
What is the fastest way to learn Rust programming if you already know another language? Learn ownership first, then everything else follows. Rust replaces the garbage collector with a compile time rule: each value has one owner, and dropping the owner frees the value. Once that clicks, borrowing, lifetimes, and the borrow checker stop feeling arbitrary and start reading as ordinary scoping rules.
Section 01 · The trade
Why Rust is worth the learning curve
Rust makes one trade that no mainstream language made before it. It gives you the speed and control of C while proving, at compile time, that your program does not read freed memory, does not race on shared state, and does not dereference a dangling pointer.
The proof happens before the binary exists. If the program compiles, an entire class of crashes is gone.
You pay for this. Compiles are slower than Go. The learning curve is real, and it is concentrated almost entirely in the first two weeks, when the borrow checker rejects code you are certain is correct. Most engineers describe the same arc: a fortnight of frustration, then the rules internalize and stop being visible.
Compare the alternatives honestly. Go, Python, and Java hand memory management to a garbage collector, which is a fine trade for most services. You pay at run time instead, in collection pauses and in memory headroom you cannot reclaim. C hands you everything and guards nothing, so the same bugs ship to production and surface at three in the morning. Rust moves the cost to the compiler, where a failure is a red message on your screen rather than a page.
If you write smart contracts, systems tooling, or anything where a memory bug is a security incident rather than an inconvenience, that trade is worth making. It is the reason Solana chose Rust for on chain programs, a comparison I unpack in Solidity vs. Rust for smart contracts.
Section 02 · Setup
Installing the toolchain and running your first program
Rust ships as one installer. rustup manages toolchain versions, cargo manages projects and dependencies, and rustc is the compiler you will rarely call directly.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustc --versionCreate and run a project with cargo, not rustc. Cargo handles the build, the dependency graph, and the test runner.
cargo new hello
cd hello
cargo runThat gives you src/main.rs containing the smallest complete Rust program:
fn main() {
println!("Hello, world!");
}println! ends with an exclamation mark because it is a macro, not a function. Macros expand at compile time and can take a variable number of arguments, which ordinary Rust functions cannot.
Section 03 · Bindings
Variables, mutability, and shadowing
In Rust a binding is immutable unless you say otherwise. This is the first place the language will surprise you.
let count = 5;
count = 6; // error[E0384]: cannot assign twice to immutable variableAdd mut and the assignment compiles:
let mut count = 5;
count = 6; // fineThe default is flipped on purpose. Most values in most programs are never reassigned, so the language makes the common case the quiet one and forces you to announce mutation. When you read Rust written by someone else, every mut is a signal that something changes over time, and that is exactly where bugs live.
Shadowing looks like mutation but is a different mechanism. You redeclare a name with a fresh let, and you may change its type while you do it.
let spaces = " "; // &str
let spaces = spaces.len(); // usize, new binding, same nameThis is why input parsing reads so cleanly in Rust. You take a string, shadow it with the parsed number, and the string binding is gone. Mutation could not have changed the type.
Section 04 · Types
Scalar and compound types
Rust is statically typed and infers most annotations, but integer width is something you choose and the choice is visible.
Scalar types cover integers (i8 through i128, u8 through u128, plus isize and usize), floats, bool, and char. A char is four bytes and holds any Unicode scalar value, not one byte as in C.
let a: u8 = 255;
let b: i64 = -9_000_000_000;
let c: f64 = 3.14159;
let d: bool = true;
let e: char = 'ß';Explicit width is not bureaucracy. It is how Rust catches the overflow that other languages swallow. In a debug build, adding one to a u8 holding 255 panics and tells you exactly where. In a release build it wraps, and you opt into that behavior deliberately with wrapping_add, or you handle it with checked_add, which returns an Option.
let x: u8 = 255;
match x.checked_add(1) {
Some(v) => println!("{v}"),
None => println!("overflow"),
}If you are choosing widths for on chain state, where every byte is paid for, the sizing rules are worth reading closely in Rust data types for Solana.
Compound types are tuples and arrays. Tuples group a fixed number of values of different types. Arrays hold a fixed number of values of one type, on the stack.
let point: (i32, i32) = (3, 7);
let (x, y) = point;
let week: [&str; 3] = ["mon", "tue", "wed"];A Vec<T> is the growable heap allocated cousin of the array, and it is what you will actually reach for most of the time.
Section 05 · The core idea
Ownership, moves, and borrowing
Every value in Rust has exactly one owner. When the owner goes out of scope, the value is dropped and its memory is freed. No garbage collector runs, and no free call appears in your code.
{
let s = String::from("hello"); // s owns the heap buffer
} // s leaves scope, buffer freed hereAssigning a heap owning value to a new binding does not copy it. It moves it, and the old binding stops being usable.
let a = String::from("hello");
let b = a; // ownership moves from a to b
println!("{a}"); // error[E0382]: borrow of moved valueThis is what stops two owners from freeing the same memory. It also means passing a value to a function gives that function ownership, and you do not get the value back unless the function returns it.
Borrowing is the escape hatch. A reference lets a function read or mutate a value without owning it. The rule the borrow checker enforces has two clauses, and every borrow checker error you hit is one of them. You may have any number of immutable references at once, or exactly one mutable reference, and never both at the same time.
fn length(s: &String) -> usize { s.len() } // borrows, does not own
fn shout(s: &mut String) { s.push_str("!"); } // borrows mutably
let mut msg = String::from("hi");
println!("{}", length(&msg));
shout(&mut msg);Read that rule as a data race prevention rule and it stops feeling arbitrary. Two readers cannot observe a torn write. One writer cannot be observed mid write. The compiler is running the analysis you would otherwise run in your head, every time, without getting tired.
Ownership is garbage collection moved to compile time
A tracing collector discovers when a value is safe to free while your program runs. The Rust compiler derives the same answer from scope, before your program runs. Same question, and the cost lands in a completely different place.
Ownership in the context of on chain account state has its own wrinkles, which I cover in Rust ownership for Solana developers.
| Error | What you wrote | The mechanical fix |
|---|---|---|
| borrow of moved value | Used a variable after passing it by value | Pass a reference instead, or clone if you truly need two owners |
| cannot borrow as mutable more than once | Two mutable references to the same value alive together | Shorten the first borrow scope, or restructure so only one exists |
| cannot borrow as mutable because also borrowed as immutable | Held a shared reference while taking a mutable one | Finish reading before you start writing, or drop the read binding |
| does not live long enough | Returned a reference to a local | Return the owned value, or take the buffer as a parameter |
Almost every week one error is on this table. None of them requires a lifetime annotation to fix.
Section 06 · Flow
Functions, control flow, and expressions
A function declares parameter types and return type. The last expression is the return value, with no return keyword and no semicolon.
fn add(a: i32, b: i32) -> i32 {
a + b
}Adding a semicolon to a + b turns it into a statement, the function returns the unit type, and the compiler tells you so. This trips up everyone once.
if is an expression, so it produces a value. Loops come in three shapes. loop runs forever until you break, and break can carry a value out. while tests a condition. for iterates anything iterable, and it is what you want in almost every case.
let grade = if score >= 90 { "A" } else { "B" };
for n in 1..=5 {
println!("{n}");
}Section 07 · Failure
Errors with Result and Option
Rust has no exceptions and no null. That sounds austere. It removes two of the most common sources of production incidents.
Absence is modelled by Option, which is either Some or None. Failure is modelled by Result, which is either Ok or Err. Both are ordinary enums, and the compiler will not let you read the value without acknowledging the other case.
fn parse(input: &str) -> Result<i32, std::num::ParseIntError> {
input.trim().parse::<i32>()
}
match parse("42") {
Ok(n) => println!("got {n}"),
Err(e) => println!("failed: {e}"),
}Handling every error at every call site would be unbearable, so Rust gives you the question mark operator. Applied to a Result, it unwraps an Ok and returns early on an Err, propagating it to the caller.
fn double(input: &str) -> Result<i32, std::num::ParseIntError> {
let n = input.trim().parse::<i32>()?;
Ok(n * 2)
}That single character is why idiomatic Rust error handling reads as linearly as exception based code while remaining explicit in the type signature. A function that can fail says so in its return type, and you cannot forget to look.
unwrap and expect are a liability on untrusted input
Both bypass the type system and panic on failure. They are correct in tests, in prototypes, and where you can prove the failure is impossible. In a service handling input you do not control, they turn a recoverable error into a crash.
Section 08 · Practice
Where to go next
Reading about ownership takes you about as far as reading about swimming. Build something small enough to finish and real enough to break.
The natural next step is a program that reads input you do not control, parses it, and fails gracefully — which exercises Result, match, and borrowing in one sitting. That is exactly what building a command line calculator in Rust walks through, including the three inputs that break the naive version.
If you came here on the way to on chain development, the same ownership rules govern account state once you reach the Anchor framework. The concepts do not change. Only the thing being owned does.
Wrap up
Ownership is the whole language
Everything else is syntax you already know wearing different clothes.
If your team is weighing Rust for on chain or systems work and wants a second opinion before committing, my blockchain development practice covers exactly that decision.