Skip to content

We are working on this site. Want to help? Open an issue or a pull request on GitHub.

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

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

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

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.

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),
    }
}

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);
}

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.

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

Section titled “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

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),
        }
    }
}
// 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;
  }
}
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)
}

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.