Skip to content

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 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

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 T
  • Err(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

Option<T> is for handling the absence of a value:

  • Some(T): Contains a value of type T
  • None: 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

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 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

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:

  1. If the Result is Ok(v), it extracts the value v and continues
  2. If the Result is Err(e), it returns early with that error
  3. 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

JavaScriptRustNotes
ExceptionsResultRust makes errors part of the function signature
null/undefinedOptionRust forces you to handle the absence of a value
try/catch blocksmatch/? operatorRust error handling is expression-based
Runtime errorsCompile-time checksRust catches many error handling mistakes at compile time
Implicit error propagationExplicit error propagationIn 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 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

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

Rust’s approach to error handling is one of its most distinctive features compared to JavaScript:

  1. Errors are values, not exceptions
  2. Error handling is explicit in type signatures
  3. The compiler enforces handling errors
  4. The ? operator makes propagating errors ergonomic
  5. 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.