Chapter 04
Rust has no try/catch for ordinary failures. A function that can fail says so in its return type, and the caller can't ignore it. It feels heavy for one day, then feels like a missing C# feature.
Fallible functions return Result<T, E> — an enum that's either Ok(value) or Err(error). It's Option's cousin: where Option models "maybe absent," Result models "maybe failed, and here's why."
fn parse_port(s: &str) -> Result<u16, ParseIntError> {
let p: u16 = s.parse()?; // note the ?
Ok(p)
}
In C#, int.Parse throwing is invisible in the signature — you only learn it can fail by reading docs or getting bitten at runtime. In Rust the Result in the return type makes fallibility part of the contract the compiler enforces.
That ? is what makes Result ergonomic in practice. On a Result, it means: if Ok, unwrap the value and continue; if Err, return it from the current function immediately. It's early-return-on-error without the ceremony — the equivalent of the rethrow boilerplate you'd otherwise write in a try/catch.
try {
var cfg = ReadFile(path);
var port = int.Parse(cfg);
return port;
} catch (Exception e) {
throw; // propagate
}
fn load(path: &str) -> Result<u16, Error> {
let cfg = read_file(path)?;
let port = cfg.parse()?;
Ok(port)
}
Each ? is a potential early exit. The happy path reads top-to-bottom with no nesting, and you literally cannot forget to handle an error — omitting the ? gives you a Result you have to deal with anyway.
Rust does have panic! — an unwinding abort, the nearest thing to an unhandled exception. It's for bugs and unrecoverable states, not control flow. .unwrap() and .expect("msg") panic if a Result/Option isn't the happy case.
| Situation | C# reflex | Rust idiom |
|---|---|---|
| User typed a bad number | catch FormatException | return Result, handle it |
| File might not exist | catch IOException | return Result, use ? |
| Invariant that "can't" be false | Debug.Assert | .expect() / panic! |
| Startup config truly missing | throw & crash | .unwrap() (fine here) |
It's tempting to .unwrap() everywhere to silence the compiler while learning. That's the equivalent of swallowing every exception with an empty catch — it compiles but you've thrown away the safety. Reach for ? and proper Result types in real code; save unwrap for prototypes and genuine impossibilities.
Hand-writing an error enum for every function is tedious, so the ecosystem standardized two crates you'll meet immediately:
| Crate | Use it for | C# feeling |
|---|---|---|
thiserror | defining your own typed error enums (libraries) | custom exception classes |
anyhow | "just give me one error type" (apps) | catching Exception at the top |
The mental shift: errors flow through your code as ordinary data you can match on, store, map, and combine — not as an out-of-band stack-unwinding mechanism.