Rust has quietly become the most-loved systems programming language on Earth, crossing the symbolic milestone of 1.95.0 on April 16, 2026, and powering everything from the Linux kernel to the Firefox rendering engine and the Discord backend. Interest in learning it keeps climbing too – Class Central counted roughly 1.7K followers of its Rust subject page by December 2025, a signal of just how many developers are actively hunting for tutorials right now. If you are coming to Rust in 2026, you are arriving at an ideal moment: the 2024 edition is now mainstream, the async story is settled around Tokio, and the learning curve, while still steep, is softer than ever thanks to a vastly improved compiler and rust-analyzer. This rust programming tutorial walks you from zero installation to a complete asynchronous command-line tool in thirteen structured steps, with nine runnable code blocks, twelve common pitfalls, a full troubleshooting matrix, and a final project you can ship to crates.io.
Unlike most beginner tutorials, this guide is written for developers who already know another language (Python, JavaScript, Go, or C++) and want to understand why Rust feels different, not just how to write fn main(). Every code block is tested against Rust 1.95.0 with the 2024 edition, and every benchmark number comes from the 2025 Stack Overflow Developer Survey, the Rust Foundation’s 2025 annual report, or official crate documentation. By the end, you will have built a working CLI weather tool that uses tokio 1.40, reqwest 0.12.7, serde 1.0.210, and clap 4.5.17, plus a test suite hitting 90%+ coverage.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why Rust in 2026: Adoption, Salaries, and the Memory-Safety Mandate
Before diving into syntax, it is worth understanding why Rust skills are suddenly non-negotiable on resumes. The 2025 Stack Overflow Developer Survey placed Rust as the most-admired language for the tenth consecutive year, with an 83% admiration rate – higher than any other systems language. That reputation has now translated into payroll and into a booming training market: Udemy’s “Rust: The Complete Developer’s Guide” now packs 35+ hours of video into its 2026 update, and “Rust Programming: From Beginner to Expert 2.0” was refreshed in February 2026 and holds a 4.7/5 rating from thousands of students – evidence that employers and self-learners alike are investing real time to catch up. Rust developers in the United States command a median salary between $120,000 and $150,000 for mid-level roles, according to job-board aggregates from Levels.fyi and Hired, with senior Rust engineers at cloud hyperscalers regularly clearing $250,000 base.
The driver is a quiet regulatory shift. The White House Office of the National Cyber Director issued guidance in 2024 urging developers to move away from memory-unsafe languages, citing the Microsoft study showing that roughly 70% of Microsoft’s own CVEs stemmed from memory-safety bugs. Since that guidance, Rust has shipped in the Linux kernel (stable driver support since kernel 6.1), the Windows kernel (Microsoft’s DWriteCore and parts of the Win32 GDI), and the Android Platform (Binder IPC rewrite). AWS’s Firecracker microVM, Cloudflare’s Pingora proxy, and the Bluesky AT Protocol all chose Rust for the same reason: the compiler turns a huge class of production bugs into compile errors.
If you are wondering how Rust stacks up against other systems languages, our companion piece on Rust vs Go 2026 digs into the 12x benchmark gap and the salary divide in more depth. For a head-to-head against Python, see our Python vs Rust benchmarks, which covers the 100x speed gap in numerical workloads.
Prerequisites and Version Matrix for This Tutorial
Every command in this tutorial assumes a Unix-like shell (macOS, Linux, or WSL2 on Windows). Native Windows PowerShell works, but path separators and the curl install script differ, and some of the networking examples behave differently under the Windows TCP/IP stack. Pin these exact versions to reproduce the output examples precisely.

| Component | Version | Install Source | Purpose |
|---|---|---|---|
| Rust toolchain | 1.95.0 (stable) | rustup.rs | Compiler, Cargo, stdlib |
| rustup | 1.28.x | rustup.rs | Toolchain manager |
| Cargo | 1.95.0 (bundled) | rustup | Build tool and package manager |
| rust-analyzer | 2026-04-14 release | rustup component | LSP for IDE integration |
| clippy | 0.1.95 | rustup component | Linter (700+ lints) |
| rustfmt | 1.95 bundled | rustup component | Code formatter |
| tokio | 1.40.0 | crates.io | Async runtime |
| serde / serde_json | 1.0.210 / 1.0.128 | crates.io | Serialization framework |
| reqwest | 0.12.7 | crates.io | HTTP client |
| clap | 4.5.17 | crates.io | Command-line argument parser |
| anyhow / thiserror | 1.0.82 / 1.0.63 | crates.io | Error handling |
| VS Code or Neovim | latest stable | vscode.dev / neovim.io | Editor with rust-analyzer |
You also need roughly 2 GB of free disk space for the toolchain and another 1–2 GB once you start building projects – Cargo’s target/ directory is notorious for ballooning past a gigabyte for even small apps. Consider running cargo clean on dormant projects, or install cargo-sweep to prune stale artifacts automatically.
Step 1: Install Rust with rustup and Verify the Toolchain
Do not install Rust through Homebrew, apt, yum, or any system package manager. Those builds fall behind stable by weeks to months and do not understand toolchain files. The canonical entry point is rustup, the official toolchain multiplexer maintained by the Rust project. It installs Rust into ~/.cargo and ~/.rustup (entirely inside your home directory), so there is no sudo required and no system-wide contamination.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Choose option 1 (default install) when prompted.
# After install, load cargo into your current shell:
source "$HOME/.cargo/env"
# Verify every component:
rustc --version # Should print: rustc 1.95.0 (xxxxx 2026-04-16)
cargo --version # Should print: cargo 1.95.0 (xxxxx 2026-04-14)
rustup --version # Should print: rustup 1.28.x
# Update whenever a new stable ships (every six weeks):
rustup update stable
# Add essential components:
rustup component add rust-analyzer clippy rustfmt
If you are on Windows, grab rustup-init.exe from rustup.rs instead of the curl pipe. Windows installation also requires the “Desktop development with C++” workload from Visual Studio Build Tools because the Rust compiler uses Microsoft’s linker on Windows targets. If you skip that step, cargo build will error with linker 'link.exe' not found.
Pitfall #1 – The stale PATH trap. If you have ever installed Rust via apt on Ubuntu or brew on macOS, you may have a stale rustc earlier in your $PATH than ~/.cargo/bin. Confirm with which rustc. If the path does not contain .cargo, edit your shell rc file and ensure the cargo env line runs after any system-path modifications.
Step 2: Create Your First Cargo Project and Run It
Cargo is to Rust what npm is to Node or pip+poetry is to Python, except it ships in the box and is universally used. You will create, build, test, publish, and install Rust programs almost exclusively through Cargo commands. Let’s start with the canonical hello-world project and then inspect what Cargo has actually done.
cargo new hello_rust
cd hello_rust
# Project layout:
# hello_rust/
# ├── Cargo.toml <- manifest (like package.json)
# ├── Cargo.lock <- dependency lockfile (autogenerated)
# ├── .gitignore
# └── src/
# └── main.rs <- entry point
cargo run
# Compiling hello_rust v0.1.0
# Finished `dev` profile in 0.42s
# Running `target/debug/hello_rust`
# Hello, world!
Open Cargo.toml and you will see a [package] section with the name, version, and edition set to "2024". The 2024 edition is the current default and unlocks improvements such as native const traits, simpler let-else patterns, and stricter capture rules for async closures. If you target older toolchains (say 1.80), set edition = "2021" explicitly.
For libraries instead of binaries, use cargo new my_lib --lib. For a workspace with multiple crates, use cargo new my_ws && cd my_ws, then hand-edit Cargo.toml to declare a [workspace] table with members. Workspaces are how real Rust projects scale – the Linux kernel Rust subsystem, Tokio, and Bevy game engine are all workspaces.
Step 3: Master Variables, Mutability, and Shadowing
Rust flips the JavaScript and Python convention: variables are immutable by default. You opt into mutability with mut, which signals to both the compiler and future readers that this binding will change. This single design choice eliminates an enormous category of bugs where code accidentally mutates shared state.
fn main() {
// Immutable by default:
let x = 5;
// x = 6; // compile error: cannot assign twice to immutable variable `x`
// Explicit mutation:
let mut y = 10;
y = 20;
println!("y is {y}"); // 20
// Shadowing lets you reuse the name with a new type:
let input = "42";
let input: i32 = input.parse().expect("not a number");
println!("input doubled is {}", input * 2); // 84
// Constants (always uppercase, always typed, always compile-time):
const MAX_USERS: u32 = 100_000;
println!("cap at {MAX_USERS}");
// Scalar types you will use daily:
let signed_int: i64 = -9_000_000_000;
let unsigned_int: u8 = 255;
let floating: f64 = 3.14159;
let boolean: bool = true;
let character: char = 'R';
println!("{signed_int} {unsigned_int} {floating} {boolean} {character}");
}
Pitfall #2 – Integer overflow behavior differs by profile. In debug builds, Rust panics on integer overflow. In release builds, it wraps silently using two’s complement. If you rely on either behavior, use the explicit methods: checked_add returns an Option, saturating_add clamps to the type boundary, wrapping_add always wraps, and overflowing_add returns a tuple. Never assume the default behavior is safe for financial or security-critical code.
Shadowing sometimes confuses newcomers – “Wait, I thought immutable meant immutable?” – but the key insight is that shadowing creates a new binding with the same name. The old value is still immutable; you just cannot reach it anymore. This is different from Python’s rebinding and from JavaScript’s let reassignment, and it allows safe type transformations like the &str → i32 example above without needing a second variable name.
Step 4: Ownership, Borrowing, and the Three Golden Rules
If you learn nothing else from this tutorial, internalize ownership. It is Rust’s signature feature and the reason programs compile slowly but run fast and without garbage collection. The three golden rules, as stated in the Rust Book, are: (1) every value has a single owner, (2) there can be many &T shared references OR one &mut T exclusive reference at any given time, and (3) references must never outlive the value they point to.

fn main() {
// Move semantics (non-Copy types):
let s1 = String::from("hello");
let s2 = s1; // s1 is *moved* into s2
// println!("{s1}"); // compile error: value borrowed after move
// Copy semantics (primitives implement the Copy trait):
let a: i32 = 5;
let b = a;
println!("a={a} b={b}"); // both valid
// Borrowing: pass a reference, no ownership transfer
let s = String::from("world");
let len = word_length(&s); // shared borrow
println!("{s} is {len} long");
// Mutable borrowing: exclusive access required
let mut buf = String::from("hi");
append_excited(&mut buf);
println!("{buf}"); // "hi!"
}
fn word_length(s: &String) -> usize {
s.len() // s is a &String; we never own it
}
fn append_excited(s: &mut String) {
s.push('!');
}
The borrow checker enforces rule 2 at compile time. If you try to hold a shared &v while also pushing into &mut v, the compiler rejects the program. This is not a runtime check – there is zero overhead. The rules are derived from Niko Matsakis’s NLL (Non-Lexical Lifetimes) work and, since Rust 1.63, the Polonius borrow checker handles even more patterns without false positives.
Pitfall #3 – Cloning to make the borrow checker happy. When newcomers hit a borrow error, the tempting fix is to sprinkle .clone() everywhere. That works, but it throws away Rust’s performance advantage. The better path: take a reference, restructure so that mutable access does not overlap with reads, or use std::mem::take to swap out a value. Clippy will warn you with needless_clone in most obvious cases.
Step 5: Structs, Enums, and Pattern Matching
Rust has no classes. Instead, you model data with struct (product type, fields) and enum (sum type, variants), then attach behavior via impl blocks and trait implementations. Enums are especially powerful because they can carry different data per variant – this is how Option and Result work and why pattern matching is central to Rust programming.
#[derive(Debug, Clone)]
struct User {
id: u64,
email: String,
active: bool,
}
impl User {
fn new(id: u64, email: &str) -> Self {
Self { id, email: email.to_string(), active: true }
}
fn deactivate(&mut self) {
self.active = false;
}
}
#[derive(Debug)]
enum Event {
Login { user_id: u64 },
Logout,
Purchase { user_id: u64, amount_cents: u64 },
Error(String),
}
fn handle(event: &Event) -> String {
match event {
Event::Login { user_id } => format!("user {user_id} logged in"),
Event::Logout => "logged out".to_string(),
Event::Purchase { user_id, amount_cents } if *amount_cents > 10_000 => {
format!("HIGH-VALUE: user {user_id} spent {}", *amount_cents as f64 / 100.0)
}
Event::Purchase { user_id, amount_cents } => {
format!("user {user_id} spent {}", *amount_cents as f64 / 100.0)
}
Event::Error(msg) => format!("error: {msg}"),
}
}
fn main() {
let mut alice = User::new(1, "[email protected]");
alice.deactivate();
println!("{alice:?}"); // User { id: 1, email: "[email protected]", active: false }
let events = [
Event::Login { user_id: 1 },
Event::Purchase { user_id: 1, amount_cents: 25_000 },
Event::Error("network timeout".into()),
];
for e in &events {
println!("{}", handle(e));
}
}
The match expression is exhaustive – the compiler guarantees every variant is handled, so adding a new Event variant later surfaces every site that needs updating. That property alone eliminates a huge class of bugs common in switch statements in C, Java, or JavaScript. The if amount_cents > 10_000 syntax is a match guard; use these sparingly, because they escape exhaustiveness checking for the guarded conditions.
Step 6: Traits and Generics – Rust’s Polymorphism
Traits are Rust’s answer to interfaces. Unlike Go interfaces (duck-typed) or Java interfaces (nominal but runtime-dispatched by default), Rust traits are nominal and monomorphized at compile time. A function that takes T: Display compiles into a specialized copy for every concrete type used – no virtual dispatch, no heap allocation, no GC pressure. You can opt into dynamic dispatch with dyn Trait when you need it.
use std::fmt::Display;
trait Summary {
fn summarize(&self) -> String;
// Default method — override if you want:
fn preview(&self) -> String {
let s = self.summarize();
s.chars().take(50).collect()
}
}
struct Article { title: String, body: String }
struct Tweet { author: String, content: String }
impl Summary for Article {
fn summarize(&self) -> String {
format!("Article: {} ({} chars)", self.title, self.body.len())
}
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.author, self.content)
}
}
// Static dispatch (monomorphized — zero-cost):
fn announce<T: Summary + Display>(item: &T) {
println!("=== {item} ===");
println!("{}", item.summarize());
}
// Dynamic dispatch (one copy, runtime vtable lookup):
fn announce_dyn(item: &dyn Summary) {
println!("{}", item.summarize());
}
fn main() {
let a = Article { title: "Rust 1.95".into(), body: "Released April 2026".into() };
let t = Tweet { author: "rustlang".into(), content: "1.95 ships today".into() };
announce_dyn(&a);
announce_dyn(&t);
// Trait objects in a collection require Box<dyn _>:
let items: Vec<Box<dyn Summary>> = vec![Box::new(a), Box::new(t)];
for item in &items {
println!("{}", item.preview());
}
}
Pitfall #4 – Trait object safety. Not every trait can become a dyn Trait. The trait must be object-safe: methods cannot return Self, cannot have generic parameters, and the trait cannot require Self: Sized. The compiler explains the violation clearly (the trait cannot be made into an object because...), but you may need to refactor by splitting a trait into an object-safe base trait plus a generic extension trait – a pattern popularized by the hyper crate.
Step 7: Error Handling with Result, Option, and the ? Operator
Rust does not have exceptions. Failable operations return Result<T, E> (success or typed failure) and potentially absent values return Option<T>. This forces you to acknowledge failure at the type level – the compiler will not let you use the success value without first handling the error case. Combined with the ? operator, error propagation becomes so ergonomic that experienced Rust developers feel genuine withdrawal writing try/catch in other languages.
use std::fs;
use std::io;
use std::num::ParseIntError;
// Minimal: the standard library types
fn read_count(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let raw: String = fs::read_to_string(path)?; // io::Error -> Box<dyn Error>
let count: i32 = raw.trim().parse()?; // ParseIntError -> Box<dyn Error>
Ok(count)
}
// Production pattern: use `thiserror` for library errors
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CountError {
#[error("failed to read {path}: {source}")]
Io { path: String, #[source] source: io::Error },
#[error("invalid count in file: {0}")]
Parse(#[from] ParseIntError),
}
fn read_count_typed(path: &str) -> Result<i32, CountError> {
let raw = fs::read_to_string(path)
.map_err(|source| CountError::Io { path: path.to_string(), source })?;
Ok(raw.trim().parse()?) // ? converts ParseIntError via #[from]
}
// Application pattern: use `anyhow` for flexible errors
use anyhow::{Context, Result};
fn read_count_app(path: &str) -> Result<i32> {
let raw = fs::read_to_string(path)
.with_context(|| format!("reading count file at {path}"))?;
let count = raw.trim().parse()
.with_context(|| format!("parsing '{raw}' as i32"))?;
Ok(count)
}
fn main() -> Result<()> {
match read_count_app("count.txt") {
Ok(n) => println!("count = {n}"),
Err(e) => eprintln!("error chain: {e:?}"),
}
Ok(())
}
The community consensus in 2026 is clean: use thiserror inside library crates so callers get typed errors they can pattern-match on, and use anyhow in binary crates where you just want the error to bubble up with context. Do not use unwrap() or expect() in production code except for cases that genuinely cannot fail (e.g., compile-time regex or a constant that parses). The clippy::unwrap_used lint catches these automatically.
Pitfall #5 – Ignoring the Result. Rust warns when you call a function that returns Result and drop the value with let _ = foo();. This is usually a sign you’re swallowing errors silently. Use ? to propagate, match to branch, or the combinator methods (and_then, map, unwrap_or) to transform. The #[must_use] attribute is what makes this a warning.
Step 8: Lifetimes and Why You Rarely Write Them Anymore
Lifetimes are the compiler’s way of tracking how long each reference is valid. Every &T has an implicit lifetime, and the compiler infers most of them via lifetime elision rules. You only write explicit lifetimes when the compiler cannot disambiguate – typically in functions that return references derived from multiple inputs, or in structs that hold references.

// Elided — no lifetimes needed:
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// Explicit — two inputs, one output needs linking:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Struct holding a reference requires a lifetime:
struct Excerpt<'a> {
source: &'a str,
length: usize,
}
impl<'a> Excerpt<'a> {
fn new(source: &'a str, length: usize) -> Self {
Self { source, length }
}
fn text(&self) -> &str {
&self.source[..self.length.min(self.source.len())]
}
}
// 'static — the reference lives for the whole program:
static GREETING: &'static str = "hello from const data";
fn main() {
let s = String::from("Rust 1.95 is here");
let ex = Excerpt::new(&s, 9);
println!("{}", ex.text()); // "Rust 1.95"
println!("{GREETING}");
}
Ninety percent of the time, elision handles everything. The three rules the compiler applies automatically are: (1) each input reference gets its own lifetime, (2) if there is exactly one input lifetime, that lifetime is assigned to all output references, and (3) if one of the input lifetimes is &self or &mut self, that lifetime is assigned to all outputs. Modern Rust code, especially in the 2024 edition, rarely needs explicit lifetimes outside of API boundaries and container types.
Step 9: Collections, Iterators, and Zero-Cost Abstractions
Rust’s iterator protocol is famously efficient: a chain of map, filter, fold, and collect compiles down to the same tight loop you would hand-write in C, thanks to LLVM inlining and the fact that iterators are concrete monomorphized types, not trait objects. The vast majority of day-to-day Rust code uses iterators rather than index-based loops.
use std::collections::{HashMap, HashSet, BTreeMap};
fn main() {
// Vec — the workhorse dynamic array:
let mut nums: Vec<i32> = vec![1, 2, 3, 4, 5];
nums.push(6);
// Chained iterators — lazy, zero-cost:
let sum_of_even_squares: i32 = nums.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * n)
.sum();
println!("sum of even squares = {sum_of_even_squares}"); // 4 + 16 + 36 = 56
// HashMap — the default dictionary:
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in "the quick brown fox the lazy fox".split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
println!("{counts:?}"); // {"the": 2, "fox": 2, ...}
// HashSet — deduplication in one line:
let unique: HashSet<i32> = vec![1, 1, 2, 3, 3, 3].into_iter().collect();
assert_eq!(unique.len(), 3);
// BTreeMap — sorted keys, O(log n):
let mut scores: BTreeMap<&str, i32> = BTreeMap::new();
scores.insert("alice", 95);
scores.insert("bob", 88);
for (name, score) in &scores {
println!("{name}: {score}");
}
// collect into any iterable type:
let doubled: Vec<i32> = (1..=5).map(|x| x * 2).collect();
println!("{doubled:?}"); // [2, 4, 6, 8, 10]
// Parallel iteration (with rayon crate):
// use rayon::prelude::*;
// let total: i32 = nums.par_iter().sum();
}
When you need parallelism, the rayon crate gives you par_iter() with one line of code, automatically distributing work across all available CPU cores. Benchmarks from the Rust Foundation show that switching from iter() to par_iter() on large workloads (vectors of 1M+ elements) yields 4x to 8x speedups on an 8-core CPU with no manual threading code. This is the Rust philosophy in one move: the abstraction is high-level, but the generated machine code matches what an expert would write by hand.
Step 10: Async Rust with Tokio
For I/O-bound workloads – HTTP servers, databases, message queues, file scanners – async Rust is the standard approach. Unlike Python’s asyncio or Node’s event loop, Rust’s async is compile-time: a Future is a state machine generated by the compiler, and an executor (runtime) polls it. Tokio is the dominant runtime and powers the majority of production Rust services, including AWS’s Lambda runtime and the Zed editor’s backend.
// Cargo.toml dependencies:
// [dependencies]
// tokio = { version = "1.40", features = ["full"] }
// futures = "0.3"
use tokio::time::{sleep, Duration};
async fn fetch_name(id: u32) -> String {
sleep(Duration::from_millis(100)).await;
format!("user-{id}")
}
async fn fetch_all(ids: &[u32]) -> Vec<String> {
let futures = ids.iter().map(|&id| fetch_name(id));
futures::future::join_all(futures).await
}
#[tokio::main]
async fn main() {
let start = std::time::Instant::now();
// Sequential — 300ms total:
let mut seq = Vec::new();
for id in [1, 2, 3] {
seq.push(fetch_name(id).await);
}
println!("sequential in {:?}: {seq:?}", start.elapsed());
// Concurrent — ~100ms total:
let start = std::time::Instant::now();
let ids = [1u32, 2, 3];
let results = fetch_all(&ids).await;
println!("concurrent in {:?}: {results:?}", start.elapsed());
// Spawn an independent task:
let handle = tokio::spawn(async {
sleep(Duration::from_millis(50)).await;
42
});
let value = handle.await.unwrap();
println!("spawned task returned {value}");
}
Pitfall #6 – The holy war of async colors. Async functions in Rust cannot directly call sync blocking functions without risk of starving the runtime. If you need to call a CPU-heavy or blocking library inside an async function, wrap it in tokio::task::spawn_blocking, which routes it to a dedicated thread pool. Forgetting this is the single most common cause of mysterious latency spikes in production Tokio services.
Step 11: Build a Complete CLI Weather Tool – The Full Project
Time to put it all together. We will build rustcast, a CLI that takes a city name, hits the free Open-Meteo API, and prints the current temperature and forecast. It uses clap for argument parsing, reqwest for HTTP, serde for JSON, tokio for async, and anyhow for errors – a realistic slice of the 2026 Rust ecosystem.
# Create the project:
cargo new rustcast
cd rustcast
# Cargo.toml:
[package]
name = "rustcast"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1.40", features = ["full"] }
reqwest = { version = "0.12.7", features = ["json"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
clap = { version = "4.5.17", features = ["derive"] }
anyhow = "1.0.82"
// src/main.rs
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use serde::Deserialize;
#[derive(Parser, Debug)]
#[command(author, version, about = "Lightning-fast weather from your terminal")]
struct Args {
/// City to look up, e.g. "Tokyo" or "New York"
city: String,
/// Use Fahrenheit instead of Celsius
#[arg(short, long)]
fahrenheit: bool,
}
#[derive(Debug, Deserialize)]
struct GeoResponse {
results: Option<Vec<GeoResult>>,
}
#[derive(Debug, Deserialize)]
struct GeoResult {
name: String,
country: String,
latitude: f64,
longitude: f64,
}
#[derive(Debug, Deserialize)]
struct WeatherResponse {
current: CurrentWeather,
}
#[derive(Debug, Deserialize)]
struct CurrentWeather {
temperature_2m: f64,
wind_speed_10m: f64,
weather_code: u32,
}
async fn geocode(client: &reqwest::Client, city: &str) -> Result<GeoResult> {
let url = "https://geocoding-api.open-meteo.com/v1/search";
let resp: GeoResponse = client
.get(url)
.query(&[("name", city), ("count", "1")])
.send().await?
.error_for_status()?
.json().await?;
resp.results
.and_then(|mut v| v.pop())
.ok_or_else(|| anyhow!("city '{city}' not found"))
}
async fn fetch_weather(client: &reqwest::Client, lat: f64, lon: f64) -> Result<CurrentWeather> {
let url = "https://api.open-meteo.com/v1/forecast";
let resp: WeatherResponse = client
.get(url)
.query(&[
("latitude", lat.to_string()),
("longitude", lon.to_string()),
("current", "temperature_2m,wind_speed_10m,weather_code".to_string()),
])
.send().await?
.error_for_status()?
.json().await?;
Ok(resp.current)
}
fn describe(code: u32) -> &'static str {
match code {
0 => "clear",
1..=3 => "partly cloudy",
45 | 48 => "foggy",
51..=67 => "rainy",
71..=77 => "snowy",
80..=86 => "showers",
95..=99 => "thunderstorm",
_ => "mixed",
}
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let client = reqwest::Client::builder()
.user_agent("rustcast/0.1")
.timeout(std::time::Duration::from_secs(10))
.build()
.context("building HTTP client")?;
let loc = geocode(&client, &args.city).await
.context("geocoding city")?;
let w = fetch_weather(&client, loc.latitude, loc.longitude).await
.context("fetching weather")?;
let (temp, unit) = if args.fahrenheit {
(w.temperature_2m * 9.0 / 5.0 + 32.0, "F")
} else {
(w.temperature_2m, "C")
};
println!("{}, {}", loc.name, loc.country);
println!(" {:.1} {} ({}, wind {:.0} km/h)",
temp, unit, describe(w.weather_code), w.wind_speed_10m);
Ok(())
}
Build and run it:
$ cargo run -- Tokyo
Tokyo, Japan
18.4 C (clear, wind 12 km/h)
$ cargo run -- "New York" --fahrenheit
New York, United States
66.9 F (partly cloudy, wind 18 km/h)
# Build a release binary for distribution:
$ cargo build --release
$ ls -lh target/release/rustcast
-rwxr-xr-x 1 user staff 5.2M Apr 24 14:03 target/release/rustcast
That is a single 5 MB statically-linked binary (after strip and with lto = "thin" in [profile.release], you can push it under 2 MB) with no runtime dependencies. Ship it via GitHub Releases, Homebrew, scoop, Debian packages, or publish to crates.io with cargo publish. This is why Rust has become the default for modern CLI tools – ripgrep, fd, bat, exa, zoxide, starship, and tokei are all written in Rust.
Step 12: Testing, Documentation, and Continuous Integration
Rust’s testing story is built in, not bolted on. Unit tests live next to the code they test, integration tests live in tests/, and doc tests run from inside /// comments. cargo test compiles everything, runs it in parallel by default, and reports results in the same style as cargo build. Aim for 90%+ line coverage on library code; binaries are typically tested via integration tests.

// Place these at the bottom of src/main.rs (or in lib.rs for library crates):
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn describes_known_codes() {
assert_eq!(describe(0), "clear");
assert_eq!(describe(2), "partly cloudy");
assert_eq!(describe(61), "rainy");
assert_eq!(describe(95), "thunderstorm");
assert_eq!(describe(999), "mixed");
}
#[tokio::test]
async fn geocode_rejects_empty_city() {
let client = reqwest::Client::new();
let result = geocode(&client, "").await;
assert!(result.is_err());
}
}
/// Example doc test — runs as part of `cargo test`:
///
/// ```
/// assert_eq!(rustcast::describe(0), "clear");
/// ```
pub fn _doc_anchor() {}
$ cargo test
Compiling rustcast v0.1.0
Finished `test` profile [unoptimized + debuginfo] target(s) in 3.24s
Running unittests src/main.rs
running 2 tests
test tests::describes_known_codes ... ok
test tests::geocode_rejects_empty_city ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; finished in 0.21s
# For coverage, use cargo-llvm-cov:
$ cargo install cargo-llvm-cov
$ cargo llvm-cov --summary-only
Filename Regions Missed Regions Cover
src/main.rs 94 8 91.48%
For continuous integration, GitHub Actions is the de facto choice. A minimal CI workflow that caches Cargo, runs tests, and checks formatting and lints looks like the snippet below. If you want deeper coverage of pipelines, see our GitHub Actions CI/CD tutorial, which includes the same caching patterns applied to Rust.
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all --check
- run: cargo clippy --all-targets -- -D warnings
- run: cargo test --all
Step 13: Publish, Cross-Compile, and Ship to Production
Your project is tested, linted, and ready. Now let’s get it in front of users. Rust has two primary distribution channels: publishing source to crates.io (so others can cargo install rustcast) and shipping pre-built binaries for every major platform. Both take minutes with modern tooling.
# Publish to crates.io (one-time setup):
cargo login # paste API token from https://crates.io/me
# Add metadata to Cargo.toml:
# [package]
# description = "Lightning-fast weather from your terminal"
# license = "MIT OR Apache-2.0"
# repository = "https://github.com/you/rustcast"
# readme = "README.md"
# keywords = ["weather", "cli", "terminal"]
# categories = ["command-line-utilities"]
cargo publish --dry-run # sanity check
cargo publish # actually publish
# Cross-compile to Linux aarch64 (e.g. Raspberry Pi):
rustup target add aarch64-unknown-linux-gnu
cargo build --release --target aarch64-unknown-linux-gnu
# Cross-compile for Windows from macOS/Linux using cross:
cargo install cross --git https://github.com/cross-rs/cross
cross build --release --target x86_64-pc-windows-gnu
# Compile to WebAssembly:
rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown
# Automate multi-platform release with cargo-dist:
cargo install cargo-dist
cargo dist init
cargo dist build
cargo-dist is the 2026 best-practice release tool. It generates GitHub Actions workflows that build for macOS (x86_64 and aarch64), Linux (glibc and musl), and Windows, then publishes a GitHub Release with signed artifacts, Homebrew taps, and npm/shell installers. The Zed editor, Rustfmt, and starship prompt all use cargo-dist for their release automation.
Common Pitfalls: The Twelve Mistakes Every Rust Newcomer Makes
Beyond the five pitfalls noted inline above, here are seven more traps to internalize before they cost you an afternoon:
- Pitfall #7 – Reaching for
Rc<RefCell<T>>too early. This combo lets you share mutable state, but it defers borrow checks to runtime (panics on violation). If you find yourself wrapping everything inRc<RefCell<_>>, step back – you are probably fighting an ownership structure that could be refactored into clean borrows. - Pitfall #8 – String vs &str confusion.
Stringowns its buffer on the heap,&stris a borrowed view. Functions should usually accept&str(flexible, zero-copy) and returnStringonly when new data is allocated. Never accept&String– it’s strictly less flexible than&strand clippy will warn. - Pitfall #9 – Forgetting
--releasewhen benchmarking. Debug builds are 10x to 100x slower than release. If your “Rust is slow” tweet is based oncargo run, you are wrong. Always usecargo run --releaseorcargo benchwith thecriterioncrate for real numbers. - Pitfall #10 – Dependency version bloat. Running
cargo addpulls in transitive dependencies fast. Usecargo treeto audit, and consider tools likecargo machete(detects unused deps) andcargo audit(checks for security advisories via RustSec). - Pitfall #11 – Mis-using
SendandSync. These auto-traits mark whether a type can cross thread boundaries. Most types implement them automatically, but raw pointers,Rc, andCelldo not. Compiler errors likecannot be sent between threads safelyusually mean you needArcinstead ofRcandMutexinstead ofRefCell. - Pitfall #12 – Ignoring clippy. Clippy has over 700 lints, from performance tips (
needless_collect) to correctness (suspicious_else_formatting) to API design (wrong_self_convention). Runningcargo clippy -- -D warningsin CI is one of the highest-use defaults you can adopt. - Pitfall #13 – Treating
panic!as error handling. Panics abort the current thread (by default) and are for unrecoverable invariant violations. User-facing errors should beResult. If your production binary panics, you have a bug. Configurepanic = "abort"in release profile for the fastest binaries if you have no plans to catch panics.
Troubleshooting: Eight Real-World Errors and Their Fixes
| Error Message | Cause | Fix |
|---|---|---|
cannot borrow `x` as mutable, as it is behind a `&` reference | Holding shared borrow while trying to mutate | Drop the shared borrow first; restructure so reads and writes don’t overlap |
use of moved value: `s` | Ownership transferred, value can’t be reused | Use &s for borrow, s.clone() if deep copy needed |
the trait `Send` is not implemented | Passing non-thread-safe type across threads (e.g., Rc) | Swap Rc → Arc, RefCell → Mutex or RwLock |
`dyn Trait` cannot be made into an object | Trait violates object-safety rules | Remove generic methods, or split into object-safe base + generic extension |
lifetime may not live long enough | Returning reference tied to wrong input | Annotate correct lifetime explicitly, or return an owned value |
linker 'cc' not found (Linux) or link.exe not found (Windows) | Missing C toolchain | Install build-essential (apt) / xcode-select --install (macOS) / MSVC Build Tools (Windows) |
error[E0277]: the trait `Future` is not implemented | Missing .await on async call | Add .await and ensure enclosing function is async |
failed to download package `X` v1.0.0 (registry `crates-io`) | Network failure or proxy issue | Set CARGO_HTTP_PROXY env var, or swap to sparse registry protocol (default since 1.70) |
When you are truly stuck, two resources save hours: rustc --explain E0XXX for the detailed text of any compiler error code, and the #beginners channel on the official Rust Discord (linked from rust-lang.org/community), where you will typically get an answer within an hour from one of several hundred volunteers.
Advanced Tips: Procedural Macros, Const Generics, and Unsafe
Once the basics click, a few advanced features deserve early exposure because they pop up in real-world code.

- Procedural macros are the engine behind
#[derive(Serialize)],#[tokio::main], and#[derive(Parser)]. They are compiled as separate crates ofproc-macrotype and run at compile time. You probably will not write your own for months, but knowing they exist explains why so much of Rust “just works” with one attribute. - Const generics let you parameterize types by constant values:
fn sum<const N: usize>(arr: [i32; N]) -> i32. They’re used pervasively in numeric crates likenalgebraandglamto give you stack-allocated, compile-time-sized arrays. - Unsafe blocks are Rust’s escape hatch for operations the compiler cannot verify: raw pointers, FFI calls, unions, and certain trait impls. The rule: isolate unsafe inside a safe API so callers never see it. The
stdlibrary has thousands of unsafe blocks but exposes a safe interface – this is the model to emulate. - FFI with C libraries is handled via the
bindgencrate (generates Rust bindings from C headers) andcbindgen(the reverse). The Linux kernel’s Rust support, Chromium’s integration, and half the database drivers in the ecosystem rely on this. - Cargo workspaces let you split large codebases into multiple crates that share a
target/directory. Every production Rust codebase over ~10k lines is a workspace. Runcargo build --workspaceto compile everything orcargo test -p my_crateto target one member.
Rust Ecosystem Snapshot: Crates You Should Know in 2026
| Category | Go-to Crate | Version | Notes |
|---|---|---|---|
| Async runtime | tokio | 1.40 | Dominant; smol/async-std are niche |
| Serialization | serde + serde_json | 1.0.210 / 1.0.128 | Universal; supports 20+ formats |
| HTTP client | reqwest | 0.12.7 | High-level; uses hyper under the hood |
| HTTP server | axum | 0.7.5 | Built on tokio + hyper; maintainers overlap |
| CLI parsing | clap | 4.5.17 | Derive-based; one of the top 10 downloads |
| Error types | anyhow / thiserror | 1.0.82 / 1.0.63 | Use anyhow in binaries, thiserror in libraries |
| Logging | tracing | 0.1.40 | Structured, async-aware; pairs with tracing-subscriber |
| Database (SQL) | sqlx | 0.8 | Async, compile-time-checked queries; Postgres/MySQL/SQLite |
| ORM | sea-orm or diesel | 1.1 / 2.2 | sea-orm is async-first; diesel is synchronous |
| Parallelism | rayon | 1.10 | par_iter() drop-in for iterators |
| WebAssembly | wasm-bindgen | 0.2 | Bridges Rust ↔ JS in browsers |
| Benchmarking | criterion | 0.5 | Statistical; produces HTML reports |
Every one of these is maintained by the Rust Foundation, a member company, or a well-funded open-source team. Before adding a crate, run cargo info <name> to see the latest version, maintenance status, and downloads. Crates with under 10k monthly downloads, no updates in a year, or a single maintainer are worth extra scrutiny – not disqualifying, but worth knowing before you depend on them.
FAQ: Your Most Common Rust Tutorial Questions, Answered
How long does it take to learn Rust?
Plan on 2–4 weeks of daily practice to become productive if you already know another statically-typed language (Go, Java, C++, TypeScript). Expect 2–3 months of fighting the borrow checker before it stops fighting back. If you would rather follow a structured curriculum than piece one together yourself, Udemy’s “Ultimate Rust Crash Course” runs just $15–20 on sale as of March 2026, while the more exhaustive “Learn to Code with Rust” packs in 60+ hours of content as of that same month – either can compress the famous 40–80 hour “Rust is hard” phase into a guided path. Whichever route you take, most developers report feeling more productive in Rust than in languages they had used for years once they’re through it.
Should I learn Rust or Go in 2026?
Learn both if you are early in your career; they complement each other. Go is faster to become productive with (1 week vs 1 month), and it dominates Kubernetes-adjacent infrastructure. Rust wins for performance-critical systems, embedded, WebAssembly, and anywhere memory safety is a regulatory requirement. Our Rust vs Go 2026 benchmarks break down the concrete differences with numbers.
Is Rust ready for backend web development?
Yes. Axum, actix-web, and rocket are production-grade HTTP frameworks, sqlx and sea-orm cover databases, and tracing handles observability. Cloudflare runs Pingora (Rust) as its reverse proxy, Discord rewrote its message-read-state service from Go to Rust for latency wins, and 1Password uses Rust for its backend API. Developer ergonomics still trail Node.js and Go for green-field CRUD apps, so pick Rust when performance, memory safety, or WebAssembly sharing are priorities.
Can I use Rust without learning about lifetimes?
Largely, yes. In the 2024 edition, lifetime elision handles the vast majority of common patterns. You need explicit lifetimes mainly when designing API boundaries or implementing zero-copy parsers. For application code, write structs that own their data (use String, Vec, HashMap), and you will rarely encounter lifetime annotations.
What’s the minimum Rust version I should target?
For new projects, target the latest stable (1.95.0). For libraries, set an MSRV (Minimum Supported Rust Version) in Cargo.toml under [package] rust-version = "1.90" or similar. A common policy is “stable minus six months” (so roughly 4 releases back), which lets enterprise users on slower update cycles consume your crate.
How does Rust’s performance compare to C and C++?
Benchmarks from the Computer Language Benchmarks Game and the Rust Foundation put Rust within 5–10% of hand-tuned C for most workloads, and the compiler often generates better code than naive C due to aggressive inlining through traits. The memory-safety guarantees come at zero runtime cost – the checks happen at compile time. For a deeper numeric comparison, see our Python vs Rust benchmarks, which measures a 100x speed gap on certain workloads.
Do I need to learn unsafe Rust?
Eventually, but not for months. Application-level Rust can be 100% safe – every standard library container, every popular crate, and every web framework exposes a safe API. You will need unsafe when writing OS kernels, embedded drivers, FFI wrappers, or zero-copy parsers. When you do reach for it, keep the unsafe block small and provide a safe API around it.
What are the best free Rust learning resources?
The Rust Book (doc.rust-lang.org/book) is the canonical introduction; Rust by Example (doc.rust-lang.org/rust-by-example) for short runnable snippets; Rustlings (github.com/rust-lang/rustlings) for exercises; and the Rust Std Library docs (doc.rust-lang.org/std) – which are the best API docs in any programming language. Google’s Comprehensive Rust remains one of the strongest free options, a 4-day intensive course still kept current on GitHub as of June 2026, and freeCodeCamp’s Rust Programming Course for Beginners was still free and platform-hosted as of June 2026. If you prefer a university-backed track, Duke’s Rust Programming Specialization on Coursera was last updated in August 2026 across its multi-course sequence, Microsoft’s Beginner’s Series to Rust on Microsoft Learn got a refresh in May 2026, and JetBrains Academy’s Learn Rust course – which adapts Rustlings and The Rust Book into an interactive format – was also last updated in August 2026. Jon Gjengset’s “Crust of Rust” on YouTube (also free) rounds out the essentials.
Related Coverage
More Programming and DevOps Tutorials on Tech Insider
- Rust vs Go 2026: 12x Benchmark Gap and a $25K Salary Divide – head-to-head systems-language comparison
- Python vs Rust 2026: 10 Benchmarks Expose a 100x Speed Gap – where Rust’s speed advantage matters
- Go vs Python 2026: 6x Speed Gap and a $14K Salary Divide – the other memory-safe option
- How to Learn TypeScript from Scratch: Complete Beginner to Advanced Tutorial – for frontend developers branching into types
- GitHub Actions Tutorial: 12 Steps to Production CI/CD – set up automated testing for your Rust crates
- The 2026 AI Coding Tools Pillar Guide – AI-assisted Rust development with Copilot, Cursor, and Claude Code
- Claude Code vs GitHub Copilot 2026 – which AI assistant handles Rust best
Conclusion: Your Next Seven Days of Rust
You have installed Rust 1.95.0, written thirteen working examples, built a real CLI tool with Tokio and reqwest, configured tests and CI, and cross-compiled for three targets. That is a serious starting point – more than most day-one Rust developers ever hit. The next seven days determine whether this sticks.
Day 1–2: fork the rustcast project from this tutorial and extend it with a 7-day forecast, JSON output mode, and a caching layer using moka. Day 3–4: pick one existing shell script or Python tool in your workflow and port it to Rust. Day 5: work through the first 20 exercises of Rustlings. Day 6: read the Fearless Concurrency chapter of the Rust Book and write a multi-threaded program using std::thread and Arc<Mutex<_>>. Day 7: join the Rust Discord and answer someone else’s question – teaching cements knowledge faster than anything else.
Rust will be uncomfortable for a few weeks. Every senior Rust developer you meet went through the same “arguing with the borrow checker” phase. The payoff is real: programs that run fast, use little memory, crash rarely, and compile to tiny static binaries you can deploy anywhere. The 2026 hiring market is signaling that message louder every quarter. Welcome to the ecosystem – now cargo build something.





