Python vs Rust Error Handling
Exception-based error handling in Python versus Rust's type-based Result<T, E> and Option<T> system.
Exceptions vs Result Types
Python handles errors by throwing and catching exceptions. A function that can fail raises an exception, and any caller in the chain can catch it with try/except. The exception propagates invisibly up the call stack until someone handles it or the program crashes. Rust rejects this model entirely. Functions that can fail return Result<T, E>, a type that is either Ok(value) or Err(error). The caller must explicitly handle both possibilities -- the compiler refuses to let you use the Ok value without acknowledging that an Err might be there. This is the single largest conceptual shift for Python developers learning Rust. In Python, any function call can secretly throw; in Rust, fallibility is encoded in the return type. If a function returns i32, it never fails. If it returns Result<i32, ParseError>, it might fail, and the signature tells you so. The trade-off is verbosity: every fallible call requires a match, an unwrap, or the ? operator. But the benefit is that error handling paths are never accidentally skipped -- a common source of production bugs in exception-based languages where a missing try/except means the error flies past silently (or crashes unexpectedly).
Parse a number
try:
n = int("42")
except ValueError:
n = 0let n: i32 = "42".parse().unwrap_or(0);Explicit match on Result
try:
n = int(user_input)
print(f"Got: {n}")
except ValueError as e:
print(f"Invalid: {e}")match user_input.parse::<i32>() {
Ok(n) => println!("Got: {n}"),
Err(e) => println!("Invalid: {e}"),
}Can you write this from memory?
Write a try/except skeleton that catches Exception. Use pass in both blocks.
The ? Operator
Rust's ? operator is syntactic sugar for early return on error. Placing ? after a Result expression unwraps the Ok value if present, or returns the Err from the current function immediately. This replaces the verbose match arm that would otherwise appear on every fallible call. Before ? was stabilized (Rust 1.13), developers used the try!() macro for the same purpose, and older codebases still reference it. Python has no equivalent -- exceptions propagate automatically, so there is no syntax for "unwrap or return the error." The closest Python analogue is not catching an exception and letting it propagate, which happens by default. The asymmetry is telling: Python requires explicit code to stop error propagation (try/except), while Rust requires explicit code to allow it (?). This means Rust functions that propagate errors are visually marked at every call site, making error flow readable without jumping to callee definitions. The ? operator also works with Option<T>, returning None from the function if the value is None. This makes it useful for chains of nullable lookups: config.get("db")?.get("host")?.as_str().
? operator for early return
def read_config(path):
# exceptions propagate automatically
with open(path) as f:
return json.load(f)
# FileNotFoundError or JSONDecodeError
# bubble up if not caughtfn read_config(path: &str) -> Result<Config, Box<dyn Error>> {
let text = std::fs::read_to_string(path)?;
let config: Config = serde_json::from_str(&text)?;
Ok(config)
// ? returns Err early if either call fails
}? with Option
# dict.get returns None if missing
host = config.get("db", {}).get("host")
if host is None:
return Nonelet host = config
.get("db")?
.get("host")?
.as_str();unwrap, expect, and When to Panic
Rust provides .unwrap() to extract the Ok value from a Result or the Some value from an Option, panicking if the value is Err or None. .expect("message") does the same but with a custom panic message. Python developers use these early and often when prototyping because they work like Python's assumption that operations succeed. The Rust community treats unwrap in production code as a code smell: it means you chose to crash instead of handle the error. The idiomatic progression is to start with unwrap during development, then replace each one with proper handling (?, match, or a default value) before shipping. expect is acceptable when the programmer can prove the error case is unreachable -- for example, a regex compiled from a literal string: Regex::new(r"\d+").expect("regex is valid"). The analogy in Python is an assert statement that should never fire. Rust also provides unwrap_or(default), unwrap_or_else(|| compute()), and unwrap_or_default() for supplying fallback values without panicking. These map roughly to Python's pattern of try/except with a default in the except block, or dict.get(key, default).
unwrap vs expect
# Python: just use it, exception if wrong
n = int("42") # ValueError if not a number// unwrap: panic on error
let n: i32 = "42".parse().unwrap();
// expect: panic with custom message
let n: i32 = "42".parse()
.expect("should be a valid integer");Fallback values
n = int(s) if s.isdigit() else 0
# or
try:
n = int(s)
except ValueError:
n = 0let n: i32 = s.parse().unwrap_or(0);
// or compute fallback lazily
let n: i32 = s.parse()
.unwrap_or_else(|_| compute_default());Custom Error Types
Python custom exceptions inherit from Exception: class NotFoundError(Exception): pass. Rust custom errors implement the std::error::Error trait and typically use an enum to represent multiple failure modes: enum AppError { NotFound, Unauthorized, Timeout }. Each variant can carry data: NotFound(String) holds the missing resource name. The thiserror crate automates the boilerplate (Display and Error trait implementations) with derive macros, reducing a 20-line impl block to a few annotations. The anyhow crate takes a different approach: it provides a single anyhow::Error type that wraps any error, similar to Python's broad except Exception. anyhow is popular in application code where you want to propagate errors without defining custom types, while thiserror is standard for libraries that export typed error hierarchies. Python's exception hierarchy (ValueError, TypeError, IOError) maps conceptually to Rust enum variants, but the mechanics differ. Python catches by type (except ValueError), Rust matches by variant (Err(AppError::NotFound(resource))). Both approaches let you handle specific failures while propagating unexpected ones, but Rust's pattern matching is exhaustive -- the compiler warns if you forget a variant.
Custom error type
class AppError(Exception):
pass
class NotFoundError(AppError):
def __init__(self, resource):
self.resource = resource
super().__init__(f"{resource} not found")use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("{0} not found")]
NotFound(String),
#[error("unauthorized")]
Unauthorized,
}Handling specific variants
try:
item = find(name)
except NotFoundError:
print("Missing resource")
except AppError:
print("Other app error")match find(name) {
Ok(item) => use_item(item),
Err(AppError::NotFound(_)) => {
println!("Missing resource");
}
Err(e) => println!("Other error: {e}"),
}Can you write this from memory?
Write a try/except skeleton that catches Exception. Use pass in both blocks.