What is Rust? The Language That Makes Systems Programming Safe
Rust eliminates entire categories of bugs — null pointer dereferences, buffer overflows, data races, use-after-free — at compile time with no runtime overhead. Microsoft, Google, Amazon, and the Linux kernel all use it for systems code.
Systems programming has always involved a brutal tradeoff. C and C++ give you direct memory control, no garbage collector, and performance that matches hand-written assembly. They also give you buffer overflows, use-after-free bugs, null pointer dereferences, and data races — an entire class of vulnerabilities that have caused decades of security incidents. The memory safety bugs in C/C++ code are not due to programmer carelessness. They are structural properties of languages that trust programmers with raw memory access.
Managed languages (Java, Python, Go, C#) eliminated these bugs by adding a garbage collector. Memory is managed automatically. Dangling pointers are impossible because the GC keeps objects alive until nothing references them. Data races are harder to introduce because the memory model prevents direct hardware-level data sharing. The cost is performance: GC pauses, higher memory overhead, and the inability to make precise low-level resource management decisions.
Rust offers a third option. It eliminates memory safety bugs at compile time through an ownership system, without a garbage collector. The compiler enforces rules about who owns memory and when it is freed. Violations are compile errors, not runtime crashes. The resulting programs have no GC pauses, no runtime overhead from safety checks, and performance that benchmarks comparably to C and C++.
Mozilla Research created Rust and the first stable version, 1.0, shipped in May 2015. Since then, it has won the Stack Overflow "most loved language" survey for nine consecutive years. Microsoft has been using Rust to rewrite security-critical Windows components. Google uses it in Android, Chrome, and its infrastructure. Amazon has deployed it in production services across AWS. The Linux kernel added Rust as a second supported language in version 6.1. These are not experimental adoptions. They are deliberate choices by organizations with serious reliability and security requirements.
The Ownership System
Ownership is Rust's defining feature. Every value has exactly one owner at a time. When the owner goes out of scope, the value is dropped (memory is freed). Ownership can be transferred (moved) but not duplicated by default. This set of rules, enforced at compile time, eliminates the bugs that plague C and C++ code.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s2);
}
s1 is a String — heap-allocated text. When let s2 = s1 executes, ownership of the string data moves from s1 to s2. After the move, s1 is no longer valid. Trying to use s1 after the move is a compile error:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:20
|
3 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`
4 | let s2 = s1;
| -- value moved here
5 | println!("{}", s1);
| ^^ value borrowed here after move
This error prevents use-after-move, which in C++ would be undefined behavior (using memory that might have been freed or overwritten). Rust catches it at compile time.
Borrowing and References
Moving ownership everywhere would be inconvenient. Rust's borrowing system lets you use a value without taking ownership:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
&s1 is a reference to s1. The function borrows s1 without taking ownership. When the function returns, s1 is still valid because the caller still owns it.
Mutable references work similarly but with an important restriction:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(s: &mut String) {
s.push_str(", world");
}
Rust enforces these rules at compile time:
- You can have any number of immutable references (
&T) to a value at the same time. - Or you can have exactly one mutable reference (
&mut T) to a value. - You cannot have both simultaneously.
This is why Rust eliminates data races. A data race requires two pointers to the same memory where at least one is writing, with no synchronization. Rust's borrow checker makes this impossible — you cannot have a mutable reference and any other reference to the same data simultaneously in the same scope. Data races are compile errors.
Lifetimes
References must not outlive the data they reference. Rust uses lifetime annotations to verify this at compile time:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("The longest string is: {}", result);
}
}
'a is a lifetime parameter. The signature fn longest<'a>(x: &'a str, y: &'a str) -> &'a str says: "the returned reference lives at least as long as the shorter of x and y's lifetimes." The compiler verifies that the returned reference is not used after either input goes out of scope.
In practice, the compiler infers lifetimes in most situations (lifetime elision). Explicit lifetime annotations are needed when the compiler cannot infer them — typically when a function returns a reference that could come from multiple inputs.
The Type System
Rust's type system is expressive. Enums are algebraic data types that can carry data. Pattern matching is exhaustive — you cannot accidentally miss a case.
#[derive(Debug)]
enum ApiError {
NotFound { resource: String, id: u64 },
Unauthorized { reason: String },
RateLimited { retry_after: u64 },
Internal(String),
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiError::NotFound { resource, id } =>
write!(f, "{} with ID {} not found", resource, id),
ApiError::Unauthorized { reason } =>
write!(f, "Unauthorized: {}", reason),
ApiError::RateLimited { retry_after } =>
write!(f, "Rate limited. Retry after {} seconds", retry_after),
ApiError::Internal(msg) =>
write!(f, "Internal error: {}", msg),
}
}
}
fn get_product(id: u64) -> Result<String, ApiError> {
if id == 0 {
return Err(ApiError::NotFound {
resource: "Product".to_string(),
id,
});
}
if id > 1_000_000 {
return Err(ApiError::Internal("ID out of range".to_string()));
}
Ok(format!("Product #{}", id))
}
fn main() {
let ids = vec![42u64, 0, 1_000_001];
for id in ids {
match get_product(id) {
Ok(product) => println!("Got: {}", product),
Err(ApiError::NotFound { resource, id }) =>
println!("Not found: {} #{}", resource, id),
Err(ApiError::RateLimited { retry_after }) =>
println!("Rate limited, retry in {}s", retry_after),
Err(e) => println!("Error: {}", e),
}
}
}
Result<T, E> is Rust's error handling type. Functions return Ok(value) on success or Err(error) on failure. Pattern matching with match handles both cases. Ignoring errors without explicit acknowledgment is much harder than in languages where exceptions can silently propagate.
The ? operator propagates errors:
use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
let n = s.trim().parse::<i32>()?;
Ok(n * 2)
}
s.trim().parse::<i32>()? parses the string as i32. If parsing fails, ? returns the error early from the function. If it succeeds, the unwrapped value is bound to n. This makes error propagation concise without hiding errors.
Option<T> replaces null:
fn find_user(id: u64) -> Option<String> {
if id == 1 {
Some("traven".to_string())
} else {
None
}
}
fn main() {
match find_user(1) {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}
let name = find_user(2).unwrap_or_else(|| "anonymous".to_string());
println!("User: {}", name);
if let Some(name) = find_user(1) {
println!("Short form: {}", name);
}
}
Null pointer dereferences are impossible because None cannot be used as a value. You must explicitly handle the None case. unwrap() panics on None, which is appropriate in tests but typically wrong in production code. unwrap_or, unwrap_or_else, map, and_then, and pattern matching are the idiomatic ways to handle Option.
Building a Web API with Axum
axum is the most widely used async web framework for Rust, built on tokio (Rust's async runtime) and hyper.
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::net::SocketAddr;
use tokio::net::TcpListener;
use tower_http::{cors::CorsLayer, trace::TraceLayer};
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct Product {
pub id: i64,
pub name: String,
pub slug: String,
pub price: f64,
pub stock: i32,
pub status: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Deserialize)]
pub struct CreateProduct {
pub name: String,
pub description: String,
pub price: f64,
pub stock: i32,
pub category_id: i64,
}
#[derive(Debug, Deserialize)]
pub struct ListParams {
#[serde(default = "default_page")]
pub page: u32,
#[serde(default = "default_per_page")]
pub per_page: u32,
pub status: Option<String>,
pub search: Option<String>,
}
fn default_page() -> u32 { 1 }
fn default_per_page() -> u32 { 20 }
#[derive(Serialize)]
pub struct ApiError {
pub error: String,
pub message: String,
}
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
}
async fn list_products(
State(state): State<AppState>,
Query(params): Query<ListParams>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ApiError>)> {
let per_page = params.per_page.min(100) as i64;
let offset = ((params.page.saturating_sub(1)) as i64) * per_page;
let status = params.status.as_deref().unwrap_or("active");
let products: Vec<Product> = sqlx::query_as!(
Product,
r#"SELECT id, name, slug, price::float8 as "price!", stock, status, created_at
FROM products
WHERE status = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3"#,
status, per_page, offset
)
.fetch_all(&state.db)
.await
.map_err(|e| {
tracing::error!("Database error listing products: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
error: "INTERNAL_ERROR".to_string(),
message: "Failed to fetch products".to_string(),
}),
)
})?;
let total: i64 = sqlx::query_scalar!(
"SELECT COUNT(*) FROM products WHERE status = $1", status
)
.fetch_one(&state.db)
.await
.unwrap_or(Some(0))
.unwrap_or(0);
Ok(Json(serde_json::json!({
"data": products,
"pagination": {
"page": params.page,
"per_page": per_page,
"total": total,
"pages": (total + per_page - 1) / per_page,
}
})))
}
async fn get_product(
State(state): State<AppState>,
Path(slug): Path<String>,
) -> Result<Json<Product>, (StatusCode, Json<ApiError>)> {
let product = sqlx::query_as!(
Product,
r#"SELECT id, name, slug, price::float8 as "price!", stock, status, created_at
FROM products WHERE slug = $1 AND status = 'active'"#,
slug
)
.fetch_optional(&state.db)
.await
.map_err(|e| {
tracing::error!("Database error fetching product {}: {}", slug, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
error: "INTERNAL_ERROR".to_string(),
message: "Failed to fetch product".to_string(),
}),
)
})?;
match product {
Some(p) => Ok(Json(p)),
None => Err((
StatusCode::NOT_FOUND,
Json(ApiError {
error: "NOT_FOUND".to_string(),
message: format!("Product '{}' not found", slug),
}),
)),
}
}
async fn health_live() -> Json<serde_json::Value> {
Json(serde_json::json!({"status": "alive"}))
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.json()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let db = PgPoolOptions::new()
.max_connections(25)
.acquire_timeout(std::time::Duration::from_secs(5))
.connect(&database_url)
.await
.expect("Failed to connect to database");
sqlx::migrate!("./migrations").run(&db).await
.expect("Failed to run migrations");
let state = AppState { db };
let app = Router::new()
.route("/health/live", get(health_live))
.route("/api/v1/products", get(list_products))
.route("/api/v1/products/:slug", get(get_product))
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.with_state(state);
let addr: SocketAddr = "0.0.0.0:8080".parse().unwrap();
let listener = TcpListener::bind(addr).await.unwrap();
tracing::info!("Listening on {}", addr);
axum::serve(listener, app).await.unwrap();
}
sqlx::query_as! is a compile-time verified SQL macro. It checks your SQL query against the actual database schema at compile time (using a SQLite snapshot of the schema in CI, or connecting to a live database during development). Incorrect column names, wrong types, and missing tables are all compile errors, not runtime panics.
#[tokio::main] transforms the async fn main() into a synchronous entry point that starts the Tokio async runtime. Tokio is the runtime that executes futures (Rust's async primitives), handles I/O events, and manages task scheduling — equivalent to Node.js's event loop but for Rust.
Rust for WebAssembly
Rust is the premier language for WebAssembly. The ownership system means no garbage collector is included in the Wasm binary. Rust Wasm binaries are small and start instantly. wasm-bindgen generates the JavaScript glue code automatically.
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
if n <= 1 { return n as u64; }
let mut a: u64 = 0;
let mut b: u64 = 1;
for _ in 2..=n {
let c = a + b;
a = b;
b = c;
}
b
}
#[wasm_bindgen]
pub fn process_image_grayscale(data: &mut [u8]) {
for chunk in data.chunks_mut(4) {
let r = chunk[0] as f32;
let g = chunk[1] as f32;
let b = chunk[2] as f32;
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
}
}
#[wasm_bindgen]
pub struct ImageProcessor {
width: u32,
height: u32,
data: Vec<u8>,
}
#[wasm_bindgen]
impl ImageProcessor {
#[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
Self { width, height, data }
}
pub fn apply_brightness(&mut self, amount: i32) {
for chunk in self.data.chunks_mut(4) {
chunk[0] = (chunk[0] as i32 + amount).clamp(0, 255) as u8;
chunk[1] = (chunk[1] as i32 + amount).clamp(0, 255) as u8;
chunk[2] = (chunk[2] as i32 + amount).clamp(0, 255) as u8;
}
}
pub fn pixel_count(&self) -> u32 {
self.width * self.height
}
pub fn get_data(self) -> Vec<u8> {
self.data
}
}
The #[wasm_bindgen] attribute on a struct impl exposes the type and its methods to JavaScript as if it were a class. No manual binding code. No serialization. The data lives in Wasm's linear memory and JavaScript accesses it through the generated glue.
The Cargo Ecosystem
Cargo is Rust's build tool, package manager, and test runner. It is consistently rated the best build tool across all programming languages in developer surveys.
[package]
name = "my-api"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "server"
path = "src/main.rs"
[dependencies]
axum = { version = "0.7", features = ["macros"] }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower-http = { version = "0.5", features = ["trace", "cors"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
anyhow = "1"
thiserror = "1"
[dev-dependencies]
axum-test = "14"
testcontainers = "0.20"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = "symbols"
cargo build
cargo build --release
cargo run
cargo test
cargo test -- --nocapture
cargo clippy
cargo fmt
cargo doc --open
cargo update
cargo clippy is Rust's linter. It catches common mistakes and Rust anti-patterns. cargo fmt formats code according to community standards. Running both in CI is standard practice.
[profile.release] with lto = true (link-time optimization) and codegen-units = 1 maximizes performance at the cost of longer compile times. strip = "symbols" removes debug symbols from the binary, reducing its size. These settings are appropriate for production builds.
When Rust Makes Sense
The learning curve is real. The borrow checker rejects code that is wrong in ways other languages permit. Developers transitioning from Python, Go, or JavaScript consistently report fighting the borrow checker for weeks before the ownership model becomes intuitive.
The payoff: once code compiles, entire categories of bugs are ruled out. Memory safety violations, data races, null dereferences. The class of bugs that causes most security vulnerabilities in C and C++ code. Production Rust services tend to have fewer subtle concurrency bugs because the compiler caught them before deployment.
Where Rust makes sense: systems programming and kernel modules, WebAssembly (no other language produces such small and fast Wasm binaries without a runtime), CLI tools where startup time and binary size matter, network services with extreme performance or memory requirements, embedded systems without a heap or OS, and codebases where memory safety vulnerabilities would be catastrophic (browsers, cryptography, OS components).
Where Rust is probably overkill: internal CRUD APIs where Go or Python would be faster to build and maintain, one-off scripts, applications where development speed matters more than runtime performance, domains where the entire ecosystem (ML, data science) is in another language.
Rust is not trying to replace Python for data science or JavaScript for web frontends. It is trying to replace C and C++ in the places where those languages are used because no better alternative existed — and doing it with a better safety story and better tooling. That is a genuinely important contribution to software engineering, and the adoption trajectory suggests the broader industry agrees.
Traits: The Interface System
Rust's trait system is how you define shared behavior. Traits are similar to interfaces in Go or Java, but with more expressive power.
use std::fmt;
trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
#[derive(Debug)]
struct NewsArticle {
headline: String,
author: String,
content: String,
}
impl Summary for NewsArticle {
fn summarize_author(&self) -> String {
self.author.clone()
}
fn summarize(&self) -> String {
format!("{}, by {}", self.headline, self.author)
}
}
#[derive(Debug)]
struct Tweet {
username: String,
content: String,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let article = NewsArticle {
headline: "Rust tops StackOverflow survey".to_string(),
author: "Traven".to_string(),
content: "For the ninth year in a row...".to_string(),
};
let tweet = Tweet {
username: "rustlang".to_string(),
content: "Rust 2.0 is here!".to_string(),
};
notify(&article);
notify(&tweet);
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest char: {}", largest(&chars));
}
impl Summary in a function parameter is a shorthand for a generic bounded by the trait. It accepts any type that implements Summary. Traits can provide default method implementations. Types that implement the trait can override the default or use it as-is. NewsArticle overrides summarize; Tweet uses the default.
Smart Pointers and Memory Management
use std::rc::Rc;
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
fn reference_counting() {
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a);
let c = Rc::clone(&a);
println!("Reference count: {}", Rc::strong_count(&a));
println!("a: {:?}", a);
println!("b: {:?}", b);
println!("c: {:?}", c);
}
fn interior_mutability() {
let data = RefCell::new(vec![1, 2, 3]);
data.borrow_mut().push(4);
println!("Data: {:?}", data.borrow());
}
fn thread_safe_shared_state() {
let counter = Arc::new(Mutex::new(0u64));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = std::thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Rc<T> (Reference Counted) allows multiple ownership of the same data within a single thread. The data is freed when the last Rc clone drops. Arc<T> (Atomically Reference Counted) is the thread-safe version. Mutex<T> provides interior mutability with mutual exclusion. Arc<Mutex<T>> is how you share mutable state between threads in Rust.
RefCell<T> provides interior mutability at runtime cost — it enforces the borrow rules at runtime instead of compile time. borrow_mut() panics if another borrow is already active. Use it when the borrow checker's static analysis is too conservative and you know the pattern is safe.
The Async Ecosystem
Rust's async/await syntax is stable but the runtime is pluggable. tokio is the dominant async runtime for server applications.
use tokio::time::{sleep, Duration};
use futures::future;
async fn fetch_data(id: u32) -> String {
sleep(Duration::from_millis(100)).await;
format!("data_{}", id)
}
#[tokio::main]
async fn main() {
let start = std::time::Instant::now();
let results = future::join_all(
(1..=10).map(|i| fetch_data(i))
).await;
println!("Fetched {} items in {:?}", results.len(), start.elapsed());
for r in &results {
println!(" {}", r);
}
}
future::join_all runs all futures concurrently. Ten 100ms operations complete in ~100ms total rather than 1000ms. Rust's async zero-cost abstraction means the compiler transforms async fn into a state machine at compile time. No heap allocation per await point. No GC pressure. The async overhead in Rust is genuinely minimal.
Production: Why Microsoft, Google, and Amazon Use Rust
The Microsoft Security Response Center published analysis showing that roughly 70% of CVEs in their products over the previous decade were memory safety issues. Buffer overflows, use-after-free, null dereferences. Bugs that Rust's ownership system makes impossible.
Microsoft has been rewriting security-critical Windows components in Rust since around 2020. The Windows kernel received its first Rust code in 2023. Google uses Rust in Chrome (the Chromium project now accepts Rust code), Android (significant portions of Android's OS code are being rewritten in Rust), and internal infrastructure. Amazon built parts of their Firecracker VMM (which powers AWS Lambda and AWS Fargate) in Rust, and uses it across several internal services.
The Linux kernel adding Rust as a second supported language in version 6.1 (2022) is the clearest signal that the systems programming community views Rust as production-ready for the most demanding environments. New kernel drivers can be written in Rust. The expectation is that more subsystems follow over time.
For developers: the investment in learning Rust pays off most when building infrastructure components, systems software, WebAssembly modules, or any code where memory safety bugs would be security vulnerabilities. The ownership model, once internalized, becomes a superpower rather than a hindrance — the compiler catches bugs before they ship.
Cargo Workspace and Project Organization
Real Rust projects split code into multiple crates (packages) using a workspace:
[workspace]
members = [
"crates/api",
"crates/core",
"crates/db",
"crates/auth",
]
resolver = "2"
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
anyhow = "1"
thiserror = "1"
tracing = "0.1"
Each crate/ directory has its own Cargo.toml and is an independent compilation unit. The workspace Cargo.toml at the root coordinates them. [workspace.dependencies] defines shared dependency versions that individual crates opt into with version.workspace = true, ensuring the entire workspace uses the same version of each library.
The core crate holds domain types and business logic with no framework dependencies. The db crate handles database access. The auth crate handles authentication. The api crate wires them together into the HTTP server. Each crate is testable in isolation. Compilation is parallelized across crates.
Error Handling with thiserror and anyhow
Two libraries dominate Rust error handling:
use thiserror::Error;
use anyhow::{Context, Result};
#[derive(Debug, Error)]
pub enum DatabaseError {
#[error("Record not found: {table} with id {id}")]
NotFound { table: String, id: i64 },
#[error("Connection failed: {0}")]
Connection(#[from] sqlx::Error),
#[error("Migration failed: {message}")]
Migration { message: String },
}
#[derive(Debug, Error)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] DatabaseError),
#[error("Validation error: {0}")]
Validation(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Not found: {0}")]
NotFound(String),
}
async fn get_user_orders(db: &PgPool, user_id: i64) -> Result<Vec<Order>, AppError> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
.fetch_optional(db)
.await
.map_err(DatabaseError::Connection)?
.ok_or_else(|| AppError::NotFound(format!("User {}", user_id)))?;
let orders = sqlx::query_as!(Order,
"SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC",
user.id
)
.fetch_all(db)
.await
.map_err(DatabaseError::Connection)?;
Ok(orders)
}
async fn application_main() -> Result<()> {
let pool = PgPoolOptions::new()
.connect("postgresql://localhost/myapp")
.await
.context("connecting to database")?;
let orders = get_user_orders(&pool, 42)
.await
.context("fetching orders for user 42")?;
println!("Found {} orders", orders.len());
Ok(())
}
thiserror generates Display and Error implementations from derive macros. #[from] automatically converts from the inner type using ?. #[error("...")] is the display format.
anyhow is for application-level code where you want to collect errors from many sources without defining a specific error type. context("...") adds a human-readable description of what was happening when the error occurred. The full error chain: "fetching orders for user 42: connecting to database: connection refused at 127.0.0.1:5432".
In a library crate, use thiserror to define specific error types that callers can match on. In application code (your main.rs, your CLI handlers), use anyhow::Result for convenience.
Rust's Growing Ecosystem
The Rust ecosystem in 2025 is substantially more mature than it was even three years ago.
Web frameworks: axum (Tokio project, most actively developed), actix-web (extremely fast, actor model), warp (filter-based composition). All are production-ready.
Database: sqlx (compile-time verified queries, async), diesel (full ORM, synchronous), sea-orm (async ORM built on sqlx). sqlx is the most commonly recommended for new async projects.
Serialization: serde is the de facto standard. Nearly every Rust library supports Serde serialization. serde_json, serde_yaml, bincode, postcard, and rmp-serde (MessagePack) all use the same Serde traits.
Async runtime: tokio dominates for servers. async-std exists as an alternative. For embedded and WASM, embassy provides no-std async.
CLI tools: clap for argument parsing, indicatif for progress bars, crossterm for terminal control, ratatui for TUI applications.
The standard for checking the ecosystem is crates.io (the package registry) and lib.rs (better search and discovery). When choosing a crate, look at: recent commit activity, the number of dependents (other crates that use it), documentation quality, and whether it is used by major projects.
The Learning Curve: What to Actually Expect
Rust has a reputation for being difficult to learn. The reputation is earned. The borrow checker rejects patterns that work in every other language. Code that looks correct does not compile. The error messages are excellent — among the best of any language — but they are still telling you that your mental model of memory is wrong.
Most developers hit three phases:
Phase 1 (weeks 1-4): Fighting the borrow checker. Trying to write Python or Go code in Rust syntax. Everything requires cloning. The compiler rejects half your attempts. This phase ends when you start thinking about ownership before writing code rather than after.
Phase 2 (months 2-6): Writing Rust that compiles but uses too many Arc<Mutex<>> and .clone() calls. Functional code, just not idiomatic. This phase ends when you internalize the patterns for sharing data — when to pass by reference, when to move, when to clone, when to use Arc.
Phase 3 (6 months+): Writing code where the ownership model informs your design decisions upfront. Functions that take references rather than ownership. Data structures designed around clear ownership hierarchies. The compiler feeling like a collaborator rather than an adversary.
The path through phases 1 and 2 is faster with the official Rust Book (free online, excellent), Rustlings exercises, and most importantly: writing real code and reading the error messages carefully. The error messages contain the explanation of the problem and often a suggested fix. Read them completely.
The investment is real. For projects where Rust's guarantees matter — the domains where memory bugs mean security vulnerabilities, where GC pauses are unacceptable, where binary size is constrained — it is worth it. For projects where Go or Python would do the job, it probably is not the right tradeoff.
Unsafe Rust
Rust's safety guarantees come from the borrow checker. unsafe is an escape hatch that opts specific blocks out of those guarantees.
fn split_at_unchecked(slice: &[i32], mid: usize) -> (&[i32], &[i32]) {
let len = slice.len();
let ptr = slice.as_ptr();
unsafe {
(
std::slice::from_raw_parts(ptr, mid),
std::slice::from_raw_parts(ptr.add(mid), len - mid),
)
}
}
fn main() {
let data = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_unchecked(&data, 3);
println!("Left: {:?}", left);
println!("Right: {:?}", right);
}
unsafe blocks allow raw pointer operations, calling unsafe functions, implementing unsafe traits, and accessing mutable static variables. The programmer asserts that the code is safe even though the compiler cannot verify it. The block is the programmer's promise.
unsafe is not a way to disable Rust's safety model globally. It is a localized opt-out that the compiler tracks. The goal is to confine unsafety to small, auditable blocks while the surrounding code remains fully checked. The standard library itself uses unsafe internally to implement safe abstractions — Vec<T> uses unsafe to manage heap-allocated memory, but the Vec API exposed to users is entirely safe.
When you see a security advisory for a Rust crate, it is almost always a bug in an unsafe block where the programmer's safety assertion was wrong. The fact that unsafety is explicitly marked and localized makes finding and auditing these bugs significantly easier than in C, where every pointer operation is potentially unsafe.
FFI: Calling C from Rust and Rust from C
Rust interoperates with C through its Foreign Function Interface. This lets you call existing C libraries from Rust and expose Rust functions to C code.
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn strlen(s: *const c_char) -> usize;
fn toupper(c: i32) -> i32;
}
fn rust_strlen(s: &str) -> usize {
let c_str = CString::new(s).expect("CString::new failed");
unsafe { strlen(c_str.as_ptr()) }
}
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn rust_process_string(input: *const c_char) -> *mut c_char {
if input.is_null() {
return std::ptr::null_mut();
}
let c_str = unsafe { CStr::from_ptr(input) };
let rust_str = c_str.to_str().unwrap_or("");
let processed = rust_str.to_uppercase();
CString::new(processed).unwrap().into_raw()
}
#[no_mangle]
pub extern "C" fn rust_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe { drop(CString::from_raw(ptr)) }
}
}
extern "C" declares functions using C calling conventions. #[no_mangle] prevents Rust from mangling the function name, making it callable from C by its exact name. CString handles the null-terminated string format C expects. Memory allocated in Rust and passed to C must be freed by Rust — the rust_free_string function handles this for the string allocated by rust_process_string.
FFI is how Rust integrates into existing codebases. A C application can link against a Rust library compiled as a cdylib or staticlib. A Rust application can call any C library with a C header. This interoperability is part of why organizations can adopt Rust incrementally — you do not need to rewrite everything at once. You start with the security-critical components.
Rust in 2025: Where the Language Stands
Rust releases a new stable version every six weeks. The edition system (Rust 2015, 2018, 2021) allows language-level changes without breaking existing code — you opt into the new edition per crate.
The active development areas in 2025: async traits (now stable in 1.75+), let-else (stable, enormously useful for early returns), return-position impl Trait in traits, the standard library's async support expansion, and continued progress on generic associated types.
The toolchain is mature. rustup manages Rust installations and toolchain versions. cargo handles every aspect of building, testing, and publishing. rust-analyzer provides IDE integration. The documentation toolchain (rustdoc with cargo doc) generates documentation from code comments and runs embedded code examples as tests. Code examples in documentation that do not compile are caught in CI — this is why Rust library documentation tends to be accurate.
For learning Rust in 2025, the recommended path: the official Rust Book for concepts, Rustlings for exercises, then a real project that forces you to deal with the ownership system in practice. Zero-to-Production in Rust (Luca Palmieri) is the best resource for learning production Rust web development specifically — it builds a real email newsletter service from scratch with full testing, database integration, and deployment.
The language is worth learning. Not for every project. Not as your first language. But for the domains where its guarantees matter, Rust delivers on its promise in ways that no other language at the same performance level can match.