Errors in Rust
Instead of using runtime exceptions (try / catch) or silent null / -1 return codes, Rust treats errors as explicit, type-checked values and separates all failures into two distinct categories: recoverable errors (represented by Result<T, E> and Option<T>) and unrecoverable errors (handled by panic!).
How Rust Compares to Other Error-Handling Models
| Error Model | Languages | How Failures Are Signaled | Drawbacks Avoided by Rust |
|---|---|---|---|
Exceptions (try / catch) |
C++, Java, Python, C#, JS | Throws an exception that unwinds the stack invisibly until caught | Hidden control flow; function signature does not show which exceptions can be thrown |
Error Codes / errno |
C, Go (val, err) |
Returns -1, NULL, or an (val, err) tuple |
Callers can accidentally ignore the error and read an invalid or zeroed return value |
| Type-Driven Values + Panics | Rust | Result<T, E> / Option<T> for recoverable conditions; panic! for bugs |
Impossible to read the success value T without first handling Err(E) or None! |
Rust's Two-Tier Error Architecture:
+-------------------------------------------------------------------+
| Failure Occurs |
+---------------------------------+---------------------------------+
|
+-----------------------+-----------------------+
| |
v v
Expected / Recoverable Failure Bug / Broken Contract
(File missing, invalid user input, (Out-of-bounds index `arr[99]`,
network timeout, parse error) division by zero, failed `assert!`)
| |
v v
`Result<T, E>` or `Option<T>` `panic!(...)`
- Encoded in function signature - Unwinds stack (runs `Drop`) or aborts
- Propagated cleanly with `?` - Halts current thread immediately
- Compiler warns if ignored (`#[must_use]`) - Not meant to be caught for normal flow
Recoverable Errors: Result<T, E> and Option<T>
When an operation might fail during normal operation—such as parsing user input, opening a configuration file, or querying a database—the function returns a value whose type encodes both the success path and the failure path:
Result<T, E>: Used when failure carries diagnostic informationEexplaining why the operation failed:Ok(T)holds the successful value of typeT.Err(E)holds the error value of typeE.
Option<T>: Used when a value may simply be absent (with only one obvious reason for absence, such as looking up a missing key in a map or finding the first element of an empty slice):Some(T)holds the present value of typeT.Nonerepresents absence.
fn parse_port(input: &str) -> Result<u16, String> {
let port: u16 = input
.trim()
.parse()
.map_err(|e| format!("invalid port '{input}': {e}"))?;
if port == 0 {
return Err(String::from("port 0 is reserved"));
}
Ok(port)
}
fn main() {
for raw in ["8080", "0", "abc", "99999"] {
match parse_port(raw) {
Ok(port) => println!("'{raw}' -> Valid port: {port}"),
Err(reason) => println!("'{raw}' -> Error: {reason}"),
}
}
}
'8080' -> Valid port: 8080
'0' -> Error: port 0 is reserved
'abc' -> Error: invalid port 'abc': invalid digit found in string
'99999' -> Error: invalid port '99999': number too large to fit in target type
Compile-Time Protection Against Ignored Errors (#[must_use])
In C or Go, calling a function that returns an error code without checking the return value compiles silently. In Rust, Result<T, E> is annotated with the #[must_use] attribute. If you call a Result-returning function and end the statement with ; without inspecting, matching, or propagating the Result, rustc immediately emits an unused_must_use warning:
fn save_record(id: u32) -> Result<(), &'static str> {
if id == 0 { Err("invalid id") } else { Ok(()) }
}
fn main() {
save_record(0);
}
warning: unused `Result` that must be used
--> src/main.rs:6:5
|
6 | save_record(0);
| ^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
6 | let _ = save_record(0);
| +++++++
Unrecoverable Errors (panic!)
When a program encounters a bug or a violated precondition that indicates corrupt program state—such as indexing past the end of a slice (slice[10]), dividing an integer by 0, or failing an assert!—Rust triggers a panic.
By default, a panic prints a diagnostic message with the source file and line number, unwinds the stack (running the Drop destructor of every live local variable on the thread so no locks or files are left dangling), and terminates the thread.