thecodex.expert · The Codex Family of Knowledge
Rust

Error Handling and Concurrency

Errors as values, concurrency as type-safe — Rust makes both impossible to ignore and impossible to misuse.

Rust 2024 edition Memory safe Last verified:
Canonical Definition

Rust error handling uses Result<T, E> as an explicit return value — the ? operator propagates errors automatically via From conversions — while concurrency uses OS threads with Arc<Mutex<T>> for shared state and mpsc channels for message passing, with Send and Sync marker traits enforcing thread safety at compile time.

One sentence

Rust makes errors explicit: recoverable errors are values of type Result<T, E> that callers must handle, and concurrency safety is enforced by the type system — shared state requires explicit synchronisation, and the compiler verifies it.

Result and the ? operator

Rustresult.rs
use std::fs;
use std::io;
use std::num::ParseIntError;

// Result<T, E> — either Ok(value) or Err(error)
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.trim().parse::<i32>()   // returns Result<i32, ParseIntError>
}

// The ? operator — propagate error up the call stack
// If the value is Err, return early with that error
// If Ok, unwrap and continue
fn double_parse(s: &str) -> Result<i32, ParseIntError> {
    let n = s.trim().parse::<i32>()?;   // early return on parse error
    Ok(n * 2)
}

// File I/O with error handling
fn read_number_from_file(path: &str) -> Result<i32, io::Error> {
    let content = fs::read_to_string(path)?;    // Err propagated
    let n: i32 = content.trim().parse()
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    Ok(n)
}

fn main() {
    match parse_number("42") {
        Ok(n)  => println!("Parsed: {n}"),
        Err(e) => println!("Error: {e}"),
    }

    // unwrap() — panics on Err (use in tests/prototypes, not production)
    let n = parse_number("10").unwrap();

    // unwrap_or — default value on error
    let n = parse_number("bad").unwrap_or(0);

    // unwrap_or_else — compute default from the error
    let n = parse_number("bad").unwrap_or_else(|_| -1);

    // map and and_then for chaining
    let doubled = parse_number("21")
        .map(|n| n * 2);      // Ok(42) if parse succeeds
}

Custom errors and thiserror

Rustcustom_errors.rs
use std::fmt;

// Manual error type
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    InvalidInput(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e)          => write!(f, "IO error: {e}"),
            AppError::Parse(e)       => write!(f, "Parse error: {e}"),
            AppError::InvalidInput(s) => write!(f, "Invalid input: {s}"),
        }
    }
}

// From conversions — enables ? to auto-convert error types
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

// With the From impls, ? auto-converts io::Error and ParseIntError to AppError
fn load_and_parse(path: &str) -> Result<i32, AppError> {
    let content = std::fs::read_to_string(path)?;   // io::Error -> AppError::Io
    let n: i32 = content.trim().parse()?;           // ParseIntError -> AppError::Parse
    if n < 0 {
        return Err(AppError::InvalidInput("Negative numbers not allowed".into()));
    }
    Ok(n)
}

Concurrency

Rustconcurrency.rs
use std::thread;
use std::sync::{Arc, Mutex};

fn main() {
    // Spawn a thread — closure captures environment
    let handle = thread::spawn(|| {
        println!("Hello from a thread!");
    });
    handle.join().unwrap();   // wait for thread to finish

    // Move closure — transfer ownership of variables into thread
    let data = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        println!("data: {:?}", data);   // data moved into thread
    });
    handle.join().unwrap();

    // Arc<Mutex<T>> — shared mutable state across threads
    // Arc = Atomically Reference Counted (thread-safe Rc)
    // Mutex = mutual exclusion lock
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut num = counter.lock().unwrap();  // acquire lock
            *num += 1;
        }));  // lock released here when `num` goes out of scope (RAII)
    }

    for h in handles { h.join().unwrap(); }
    println!("Counter: {}", *counter.lock().unwrap());  // 10
}

Channels — message passing

Rustchannels.rs
use std::sync::mpsc;   // multiple producer, single consumer
use std::thread;

fn main() {
    // Create a channel — tx sends, rx receives
    let (tx, rx) = mpsc::channel();

    // Multiple producers — clone the sender
    let tx2 = tx.clone();

    thread::spawn(move || {
        tx.send("message from thread 1").unwrap();
    });

    thread::spawn(move || {
        tx2.send("message from thread 2").unwrap();
    });

    // Receive messages — recv() blocks until a message arrives
    // The channel closes when all senders are dropped
    for received in rx {   // rx is an iterator
        println!("Got: {received}");
    }
    println!("Channel closed");

    // try_recv() — non-blocking receive
    // recv_timeout() — receive with timeout

    // Sync channel — bounded capacity (blocks sender when full)
    let (tx, rx) = mpsc::sync_channel(10);  // buffer of 10
}

Send and Sync — thread safety via the type system

Rust's concurrency safety is enforced by two marker traits:

  • Send: a type is safe to transfer ownership to another thread. Most types are Send. Rc<T> is not Send (use Arc). Raw pointers are not Send.
  • Sync: a type is safe to share a reference to across threads (i.e., &T is Send if T is Sync). Most types are Sync. Cell<T> and RefCell<T> are not Sync.

These traits are automatically derived for types whose fields are all Send/Sync. The compiler uses them to reject code that would cause data races — at compile time, with no runtime overhead.

Rustsend_sync.rs
// Compiler prevents sharing Rc across threads
use std::rc::Rc;
use std::sync::Arc;

fn needs_send<T: Send>(_: T) {}

let rc = Rc::new(42);
// needs_send(rc);   // ERROR: Rc<i32> is not Send

let arc = Arc::new(42);
needs_send(arc);     // OK: Arc<i32> is Send + Sync

// The type system prevents data races:
// You cannot share &mut T across threads
// because &mut T is only Send if T: Send,
// and borrowing rules prevent aliasing anyway
Commonly confused
Rust has no null and no exceptions — errors are values. Recoverable errors use Result<T, E>. Unrecoverable errors use panic!(), which unwinds the stack (or aborts). There is no try/catch. The ? operator propagates errors up the call stack. This forces callers to explicitly decide what to do with errors.
unwrap() is not error handling — it is a deliberate panic choice. Calling .unwrap() on a Result or Option says: "I am certain this is Ok/Some, and if it isn't, panic." It is appropriate in tests, prototypes, and truly impossible cases. In production paths, use ?, match, or the combinator methods.
Mutex::lock() returns a LockResult — it can fail. A mutex is "poisoned" if a thread panics while holding it. lock().unwrap() propagates the poison. In most applications, treating a poisoned mutex as an error (or simply propagating the panic) is correct. The lock guard implements Drop — it is released automatically when it goes out of scope.
How this connects
Requires first
See also

The ? operator and the Try trait

The ? operator desugars to a match expression: if the value is Err(e), it calls From::from(e) to convert the error type to the function's return type, then returns early with that error. If it is Ok(v), it evaluates to v. The From conversion means you can use ? to propagate different error types as long as you implement From<SourceError> for TargetError. The nightly Try trait (RFC 3058) generalises this beyond Result/Option to any type implementing the protocol — work in progress as of Rust 2024.

Fearless concurrency: the type system proof

Rust's "fearless concurrency" claim is backed by a formal guarantee: if your Rust program compiles without unsafe, it cannot have data races. The proof: a data race requires two threads to access the same memory location simultaneously, at least one access being a write, without synchronisation. The ownership system prevents the "simultaneous access" case — you cannot have two mutable references to the same data (&mut T is exclusive). The Send/Sync marker traits prevent transferring non-thread-safe types across thread boundaries. Mutex, RwLock, and atomic operations provide the synchronised access path. Together, these form a complete exclusion of the data race condition.

Sources

1
The Rust Programming Language — Ch 9: Error Handling. doc.rust-lang.org/book/ch09-00-error-handling.html.
2
The Rust Programming Language — Ch 16: Fearless Concurrency. doc.rust-lang.org/book/ch16-00-concurrency.html.
3
Rust std — std::sync. doc.rust-lang.org/std/sync/.
4
RFC 3058 — Try Trait v2. rust-lang.github.io/rfcs/3058-try-trait-v2.html.
Source confidence: High Last verified: Primary source: The Rust Book · doc.rust-lang.org/book/

Sources

1
The Rust Book — Ch 9: Error Handling. doc.rust-lang.org/book/ch09-00-error-handling.html.
2
The Rust Book — Ch 16: Fearless Concurrency. doc.rust-lang.org/book/ch16-00-concurrency.html.
3
Rust std — std::sync. doc.rust-lang.org/std/sync/.
4
RFC 3058 — Try Trait v2. rust-lang.github.io/rfcs/3058-try-trait-v2.html.