Rust
02 / 03

Error Handling, Traits & Async

Error Handling, Traits & Async

Error Handling with Result & Option

// Option<T> — value may or may not exist
fn find_user(id: u32) -> Option<String> {
    if id == 1 { Some(String::from("Alice")) }
    else { None }
}

match find_user(1) {
    Some(name) => println!("Found: {name}"),
    None => println!("Not found"),
}

// Shorthand
let name = find_user(1).unwrap();              // panics if None
let name = find_user(1).expect("User not found"); // panic with message
let name = find_user(1).unwrap_or("Unknown".to_string());
let len = find_user(1).map(|n| n.len());       // Option<usize>
let name = find_user(1)?;                      // propagate None as return

// Result<T, E> — success or error
use std::fs;
use std::io;

fn read_file(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?;  // ? propagates error
    Ok(content)
}

match read_file("config.txt") {
    Ok(content) => println!("{content}"),
    Err(e) => eprintln!("Error: {e}"),
}

// ? operator — early return on error
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
    let n: i32 = s.trim().parse()?;  // propagate ParseIntError
    Ok(n * 2)
}

// anyhow for application error handling
use anyhow::{Result, Context};
fn run() -> Result<()> {
    let data = fs::read_to_string("config.json")
        .context("Failed to read config")?;
    Ok(())
}

Traits

// Trait — like an interface
trait Greet {
    fn greet(&self) -> String;
    fn farewell(&self) -> String {   // default implementation
        String::from("Goodbye!")
    }
}

struct English;
struct Spanish;

impl Greet for English {
    fn greet(&self) -> String { String::from("Hello!") }
}

impl Greet for Spanish {
    fn greet(&self) -> String { String::from("¡Hola!") }
    fn farewell(&self) -> String { String::from("¡Adiós!") }
}

// Trait as parameter
fn say_hello(lang: &impl Greet) {
    println!("{}", lang.greet());
}

// Trait bound syntax (equivalent)
fn say_hello<T: Greet>(lang: &T) {
    println!("{}", lang.greet());
}

// Dynamic dispatch (trait object — runtime polymorphism)
fn say_hellos(langs: &[Box<dyn Greet>]) {
    for lang in langs {
        println!("{}", lang.greet());
    }
}

// Derive common traits
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
struct Config { ... }

// Common standard traits
// Display — for println!("{}", val)
// From/Into — type conversion
// Iterator — enables for loops and iterator adapters
// Send/Sync — thread safety markers

Async/Await with Tokio

// Cargo.toml
// tokio = { version = "1", features = ["full"] }
// reqwest = { version = "0.12", features = ["json"] }

use tokio;
use reqwest;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let response = fetch_user(1).await?;
    println!("{:?}", response);
    Ok(())
}

async fn fetch_user(id: u32) -> anyhow::Result<serde_json::Value> {
    let url = format!("https://jsonplaceholder.typicode.com/users/{}", id);
    let resp = reqwest::get(&url).await?.json().await?;
    Ok(resp)
}

// Parallel tasks
use tokio::task;

let (r1, r2) = tokio::join!(
    fetch_user(1),
    fetch_user(2),
);

// Spawn concurrent task
let handle = tokio::spawn(async move {
    // runs concurrently
    heavy_async_work().await
});
let result = handle.await?;   // join

// Channels
use tokio::sync::mpsc;
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move { tx.send("hello").await.unwrap(); });
while let Some(msg) = rx.recv().await {
    println!("{msg}");
}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free