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
JavaScript uses exceptions and the try/catch mechanism for error handling:
// JavaScript error handlingfunction 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
Rust doesn’t use exceptions. Instead, it has two main enum types for handling errors and absence of values:
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 Resultfn 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
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 Optionfn 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
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
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
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
For quick prototyping or when you’re certain an operation will succeed:
// Unwrap - panics if the Result is Err or Option is Nonelet value = divide(10.0, 2.0).unwrap();
// Expect - like unwrap but with a custom error messagelet 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
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
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
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
JavaScript Best Practices
// Good JavaScript error handlingasync 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
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::Errorimpl 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
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.