Error Handling in Rust
Error handling is an essential part of robust software development. Let’s explore how Rust handles errors compared to JavaScript.
JavaScript’s Approach: Exceptions
Section titled “JavaScript’s Approach: Exceptions”JavaScript uses exceptions and the try/catch mechanism for error handling:
// JavaScript error handling
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error("An error occurred:", error.message);
}
In JavaScript:
- Functions can throw exceptions anywhere
- Errors propagate up the call stack until caught
- If not caught, the program crashes with an unhandled exception
- You can throw any type of value, not just Error objects
- Errors are handled at runtime
Rust’s Approach: Result and Option Types
Section titled “Rust’s Approach: Result and Option Types”Rust doesn’t use exceptions. Instead, it has two main enum types for handling errors and absence of values:
The Result Type
Section titled “The Result Type”Result<T, E>
is an enum with two variants:
Ok(T)
: Holds a success value of type TErr(E)
: Holds an error value of type E
// Rust error handling with Result
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
return Err(String::from("Cannot divide by zero"));
}
Ok(a / b)
}
fn main() {
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(error) => println!("Error: {}", error)
}
}
The Option Type
Section titled “The Option Type”Option<T>
is for handling the absence of a value:
Some(T)
: Contains a value of type TNone
: Represents no value
// Rust handling absence of values with Option
fn find_user(id: u32) -> Option<String> {
if id == 42 {
Some(String::from("Alice"))
} else {
None
}
}
fn main() {
match find_user(42) {
Some(name) => println!("User found: {}", name),
None => println!("User not found")
}
}
Comparison: JavaScript null vs Rust Option
Section titled “Comparison: JavaScript null vs Rust Option”In JavaScript, you might represent the absence of a value with null
or undefined
:
function findUser(id) {
if (id === 42) {
return "Alice";
} else {
return null;
}
}
const user = findUser(999);
if (user) {
console.log("User found:", user);
} else {
console.log("User not found");
}
Issues with this approach:
- Nothing enforces checking for null
- TypeError if you try to access properties on null
- You can’t tell from a function’s signature if it might return null
Rust’s Option<T>
makes the possibility of no value explicit and forces you to handle it.
Working with Result and Option
Section titled “Working with Result and Option”Using match
Section titled “Using match”The most explicit way to handle Result and Option is with match
:
fn main() {
let result = divide(10.0, 2.0);
match result {
Ok(value) => println!("Success: {}", value),
Err(e) => println!("Error: {}", e),
}
}
Using if let
Section titled “Using if let”For simpler cases where you only care about one variant:
if let Ok(value) = divide(10.0, 2.0) {
println!("Success: {}", value);
}
if let Some(name) = find_user(42) {
println!("Found user: {}", name);
}
Using unwrap and expect
Section titled “Using unwrap and expect”For quick prototyping or when you’re certain an operation will succeed:
// Unwrap - panics if the Result is Err or Option is None
let value = divide(10.0, 2.0).unwrap();
// Expect - like unwrap but with a custom error message
let user = find_user(42).expect("Failed to find user");
Warning: Using unwrap()
and expect()
will cause your program to panic (crash) if there’s an error, similar to an unhandled exception in JavaScript.
The ? Operator
Section titled “The ? Operator”Rust has a convenient ?
operator for propagating errors:
fn divide_and_multiply(a: f64, b: f64, c: f64) -> Result<f64, String> {
// ? will return early if divide returns an Err
let division_result = divide(a, b)?;
// This only runs if divide was successful
Ok(division_result * c)
}
fn main() {
match divide_and_multiply(10.0, 0.0, 2.0) {
Ok(result) => println!("Result: {}", result),
Err(error) => println!("Error: {}", error),
}
}
The ?
operator:
- If the Result is
Ok(v)
, it extracts the value v and continues - If the Result is
Err(e)
, it returns early with that error - Only works in functions that return Result or Option
This is somewhat similar to using try/catch
in JavaScript, but it’s checked at compile time.
JavaScript vs Rust: Error Handling Paradigms
Section titled “JavaScript vs Rust: Error Handling Paradigms”JavaScript | Rust | Notes |
---|---|---|
Exceptions | Result | Rust makes errors part of the function signature |
null/undefined | Option | Rust forces you to handle the absence of a value |
try/catch blocks | match/? operator | Rust error handling is expression-based |
Runtime errors | Compile-time checks | Rust catches many error handling mistakes at compile time |
Implicit error propagation | Explicit error propagation | In Rust, you must explicitly propagate errors |
Custom Error Types in Rust
Section titled “Custom Error Types in Rust”For more complex applications, you can define custom error types:
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(String),
NetworkError { url: String, status_code: u32 },
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::IoError(e) => write!(f, "IO error: {}", e),
AppError::ParseError(s) => write!(f, "Parse error: {}", s),
AppError::NetworkError { url, status_code } =>
write!(f, "Network error: {} returned {}", url, status_code),
}
}
}
Error Handling Best Practices
Section titled “Error Handling Best Practices”JavaScript Best Practices
Section titled “JavaScript Best Practices”// Good JavaScript error handling
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching user data:", error);
// Re-throw or return a default value
throw error;
}
}
Rust Best Practices
Section titled “Rust Best Practices”use std::io::{self, Read};
use std::fs::File;
// Define your error type
#[derive(Debug)]
enum FileError {
IoError(io::Error),
EmptyFile,
InvalidContent(String),
}
// Implement From for easy conversion from std::io::Error
impl From<io::Error> for FileError {
fn from(error: io::Error) -> Self {
FileError::IoError(error)
}
}
fn read_username_from_file() -> Result<String, FileError> {
// The ? operator automatically converts io::Error to FileError
let mut file = File::open("username.txt")?;
let mut username = String::new();
file.read_to_string(&mut username)?;
if username.is_empty() {
return Err(FileError::EmptyFile);
}
if !username.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(FileError::InvalidContent(
"Username contains invalid characters".to_string()
));
}
Ok(username)
}
Conclusion
Section titled “Conclusion”Rust’s approach to error handling is one of its most distinctive features compared to JavaScript:
- Errors are values, not exceptions
- Error handling is explicit in type signatures
- The compiler enforces handling errors
- The
?
operator makes propagating errors ergonomic - Custom error types provide flexibility and precision
This approach leads to more robust code with fewer runtime surprises, but requires a different mental model from JavaScript’s exception-based approach.
In the next section, we’ll look at practical patterns for error handling in real-world Rust applications.