100 days of Rust programming: the recap, explained simply
After 100 days of working with Rust, you've probably noticed something: the language refuses to let you get away with the usual shortcuts. Rust forces you to think about memory, concurrency, and error handling upfront—which feels painful at first, but becomes your competitive advantage. This recap distils the lessons that matter most for teams building reliable systems.
Ownership is the foundation — Moves, borrows and lifetimes
Rust's ownership model is where everything starts. Unlike garbage-collected languages, Rust tracks who owns each piece of memory at compile time. Every value has one owner, and when that owner goes out of scope, the memory is freed automatically. This eliminates entire classes of bugs: use-after-free, double-free, and memory leaks simply cannot happen.
The practical impact: you spend the first few weeks fighting the borrow checker, then you stop writing entire categories of defensive code. In production services, this means fewer mysterious crashes and fewer late-night debugging sessions tracking down memory corruption.
Three concepts make this real:
- Moves transfer ownership. When you pass a value to a function, you move it unless you explicitly borrow it. This is intentional and forces you to think about who is responsible for cleanup.
- Borrows let you use a value without owning it. An immutable borrow (
&T) lets multiple readers access the same value. A mutable borrow (&mut T) is exclusive—only one mutable borrow can exist at a time. This rule prevents data races at compile time. - Lifetimes tell the compiler how long a borrow is valid. They're annotations that connect the lifetime of a reference to the lifetime of the data it points to.
Here's a concrete example: in a web service, you fetch a database record and return it to the caller.
// Ownership: the function owns the String
fn fetch_user_name(user_id: u64) -> String {
format!("User {}", user_id)
}
// Borrow: the function borrows the String, doesn't own it
fn log_user_name(name: &str) {
println!("User: {}", name);
}
// Lifetime annotation: result references the input
fn extract_domain(email: &str) -> &str {
email.split('@').nth(1).unwrap_or("")
}
fn main() {
let name = fetch_user_name(42); // name owns the String
log_user_name(&name); // pass a borrow
log_user_name(&name); // borrow again—still works
drop(name); // explicit cleanup (usually not needed)
}In a real service, this means you can safely pass references through your call stack without worrying about who cleans up. The compiler guarantees that the reference is always valid when you use it.
Types prevent bugs — Enums, Option and Result
Rust's type system is aggressive about making invalid states unrepresentable. You don't use null. You don't return magic error codes. Instead, you use types to encode the domain logic directly into your code.
The two most important types are Option<T> and Result<T, E>. They force you to handle the cases where a value might be missing or an operation might fail.
Option<T> represents "maybe a value":
// No null pointer exceptions
fn find_user(id: u64) -> Option<User> {
// returns Some(user) or None
}
// Caller must handle both cases
match find_user(42) {
Some(user) => println!("Found: {}", user.name),
None => println!("User not found"),
}Result<T, E> represents "success with a value or failure with an error":
// Function can fail—caller must decide what to do
fn parse_config(content: &str) -> Result<Config, ParseError> {
// returns Ok(config) or Err(error)
}
// Handle the result
match parse_config(content) {
Ok(config) => println!("Config: {:?}", config),
Err(e) => eprintln!("Parse error: {}", e),
}In practice, you chain these with the ? operator for cleaner code:
fn fetch_and_parse(url: &str) -> Result<Config, Box<dyn Error>> {
let content = fetch_url(url)?; // Return early if fetch fails
let config = parse_config(&content)?; // Return early if parse fails
Ok(config)
}Enums go further. You can encode domain logic as an enum, making invalid states impossible:
enum OrderStatus {
Pending { created_at: DateTime },
Processing { started_at: DateTime },
Shipped { tracking_id: String },
Delivered { delivered_at: DateTime },
Cancelled { reason: String },
}
// The compiler forces you to handle all cases
match order.status {
OrderStatus::Pending { created_at } => { /* handle pending */ },
OrderStatus::Processing { started_at } => { /* handle processing */ },
OrderStatus::Shipped { tracking_id } => { /* handle shipped */ },
OrderStatus::Delivered { delivered_at } => { /* handle delivered */ },
OrderStatus::Cancelled { reason } => { /* handle cancelled */ },
}The benefit: if you add a new status variant, the compiler forces you to update every match statement. You can't accidentally forget a case.
Async Rust powers services — Tokio, Axum and SQLx
Async Rust is where you build production services. Tokio is the runtime, Axum is the web framework, and SQLx handles database queries safely.
Tokio lets you handle thousands of concurrent connections with a thread pool, not threads-per-connection. On a typical machine, you can spawn 10,000 tasks with modest memory overhead.
use tokio::spawn;
#[tokio::main]
async fn main() {
let mut handles = vec![];
// Spawn 1000 tasks
for i in 0..1000 {
let handle = spawn(async move {
do_work(i).await
});
handles.push(handle);
}
// Wait for all to complete
for handle in handles {
let _ = handle.await;
}
}Axum is a type-safe web framework. Route handlers are async functions, and the type system ensures you extract the right dependencies:
use axum::{Router, routing::get, extract::Path, http::StatusCode, Json};
async fn get_user(Path(id): Path<u64>) -> Result<Json<User>, StatusCode> {
let user = find_user(id).await
.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(user))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users/:id", get(get_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}SQLx compiles SQL queries at build time. The compiler checks your SQL syntax and verifies that your query result types match your Rust structs:
use sqlx::PgPool;
#[derive(sqlx::FromRow)]
struct User {
id: u64,
email: String,
created_at: DateTime<Utc>,
}
async fn fetch_user(pool: &PgPool, id: u64) -> Result<User, sqlx::Error> {
sqlx::query_as::<_, User>(
"SELECT id, email, created_at FROM users WHERE id = $1"
)
.bind(id)
.fetch_one(pool)
.await
}
The trio—Tokio, Axum, SQLx—gives you a type-safe, efficient pipeline for building services. Request validation happens at compile time. Database queries are checked before you deploy. Runtime errors become increasingly rare.
Tooling makes it productive — Cargo, clippy, tests and fast binaries
Rust's tooling ecosystem is exceptional. Cargo is your build system, dependency manager, and task runner all in one. cargo build compiles your code. cargo test runs tests. cargo fmt formats code to a standard. cargo clippy lints for common mistakes.
Clippy catches things the compiler doesn't warn about:
- Unnecessary clones and allocations
- Unused variables and imports
- Inefficient patterns (like collecting into a Vec only to iterate once)
- Logic errors (like comparing floats with
==)
Running cargo clippy regularly trains your intuition for idiomatic Rust.
Testing is built into the language:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_email() {
let email = "user@example.com";
assert_eq!(extract_domain(email), "example.com");
}
#[test]
#[should_panic]
fn test_invalid_email_panics() {
extract_domain("invalid");
}
}Binaries are fast because Rust compiles to native code with no garbage collection overhead. A service written in Rust often outperforms equivalent Go or Node.js services with less memory and CPU.
In one production scenario, we migrated a latency-sensitive service from Node.js to Rust. P99 latency dropped from 40ms to 8ms, and memory usage fell by 70%. The Rust binary was also simpler to deploy—a single statically-linked executable with no runtime dependencies.
Common mistakes
Fighting the borrow checker instead of learning it. The borrow checker is not your enemy; it's catching real bugs. When you get a compilation error about borrowing, read it carefully. The error message usually tells you exactly what to fix.
Using .unwrap() in production code. unwrap() panics if the value is None or Err. In libraries and services, use proper error handling with ? or match. Reserve unwrap() for tests and prototypes.
Cloning too much. Beginners often clone data to avoid borrow checker errors. Clippy will catch many of these, but the habit takes time to break. Think about ownership: does this function need to own the data, or can it borrow?
Ignoring async/await complexity. Async Rust is powerful but introduces subtle bugs. Futures that aren't awaited silently do nothing. Data races in async code are harder to spot. Use the type system: if something is async fn, the caller must await it.
Key takeaways
- Ownership and borrowing eliminate memory bugs at compile time. Spend the first few weeks learning this; it pays dividends forever.
- Option and Result force you to handle missing values and errors explicitly. No silent failures or null pointer exceptions.
- Async Rust with Tokio, Axum, and SQLx builds efficient, type-safe services that handle thousands of connections.
- Cargo and Clippy catch mistakes before you deploy. Tests run fast. Binaries are small and efficient.
- The upfront cognitive load is real, but the payoff is fewer bugs, faster execution, and more confident refactoring.
- Rust scales from scripts to large systems. The same principles apply at 1,000 lines and 1,000,000 lines.






Comments
Loading comments…