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.
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
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
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
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
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.,
&Tis Send if T is Sync). Most types are Sync.Cell<T>andRefCell<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.
// 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 anywayResult<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.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.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.