4. Handling Errors#

Learn how to write robust programs that handle failures gracefully.

4.1. How does this language handle problems or failures without exceptions like in C++ or Python?#

Instead of throwing exceptions that can crash your program, this language uses a special type that represents either “success with a value” or “failure with an error”.

Think of it like a delivery box. It either contains your package (success) or a note explaining why delivery failed (error). You must open the box and check what’s inside.

use std::fs;

fn read_file_content(filename: &str) -> Result<String, std::io::Error> {
    fs::read_to_string(filename)  // Returns Result
}

fn main() {
    let result = read_file_content("config.txt");

    match result {
        Ok(content) => {
            println!("File content: {}", content);
        },
        Err(error) => {
            println!("Failed to read file: {}", error);
        }
    }
}

Compare to C++ exceptions:

#include <fstream>
#include <stdexcept>

std::string read_file_content(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        throw std::runtime_error("Cannot open file");  // Exception thrown
    }
    // Read file...
    return content;
}

int main() {
    try {
        std::string content = read_file_content("config.txt");
        std::cout << "File content: " << content << std::endl;
    } catch (const std::exception& e) {  // Must remember to catch
        std::cout << "Error: " << e.what() << std::endl;
    }
}

Compare to Python exceptions:

def read_file_content(filename):
    try:
        with open(filename, 'r') as file:
            return file.read()
    except FileNotFoundError as e:
        raise e  # Exception propagated

try:
    content = read_file_content("config.txt")
    print(f"File content: {content}")
except FileNotFoundError as e:
    print(f"Error: {e}")

More examples of error handling:

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    // Handle individual operations
    match divide(10, 2) {
        Ok(result) => println!("10 / 2 = {}", result),
        Err(msg) => println!("Error: {}", msg),
    }

    // Chain operations
    let result = divide(10, 2)
        .and_then(|x| divide(x, 2))  // Only runs if first succeeded
        .and_then(|x| divide(x, 2)); // Only runs if second succeeded

    match result {
        Ok(final_result) => println!("Final result: {}", final_result),
        Err(msg) => println!("Error somewhere: {}", msg),
    }
}

Key advantages:

  • The compiler forces you to handle errors

  • No silent failures or crashes

  • Clear separation between success and failure paths

  • Errors are part of the function’s type signature

You cannot accidentally ignore an error without the compiler warning you.

This type is called Result enum.

4.2. How do I manage errors easily in functions that might return a value that could fail?#

There’s a convenient operator that automatically propagates errors up the call stack, making error handling much cleaner.

use std::fs;
use std::io;

fn process_config() -> Result<String, io::Error> {
    let content = fs::read_to_string("config.txt")?;  // ? propagates errors
    let processed = content.trim().to_uppercase();
    Ok(processed)
}

fn main() {
    match process_config() {
        Ok(result) => println!("Processed config: {}", result),
        Err(e) => println!("Failed to process config: {}", e),
    }
}

The ? operator means: “If this is an error, return the error immediately. If it’s success, give me the value and continue.”

Without the ? operator (verbose):

fn process_config() -> Result<String, io::Error> {
    let content = match fs::read_to_string("config.txt") {
        Ok(content) => content,
        Err(e) => return Err(e),  // Manually propagate error
    };

    let processed = content.trim().to_uppercase();
    Ok(processed)
}

Chaining multiple operations:

fn complex_operation() -> Result<i32, String> {
    let value1 = get_number_from_file("file1.txt")?;
    let value2 = get_number_from_file("file2.txt")?;
    let result = divide(value1, value2)?;
    let final_result = multiply_by_two(result)?;
    Ok(final_result)
}

Compare to exception handling in other languages:

C++ (verbose error checking):

std::optional<int> complex_operation() {
    auto value1 = get_number_from_file("file1.txt");
    if (!value1.has_value()) return std::nullopt;

    auto value2 = get_number_from_file("file2.txt");
    if (!value2.has_value()) return std::nullopt;

    // ... more checks
}

Python (with exceptions):

def complex_operation():
    # If any function throws an exception, it automatically propagates
    value1 = get_number_from_file("file1.txt")
    value2 = get_number_from_file("file2.txt")
    result = divide(value1, value2)
    return multiply_by_two(result)

The ? operator gives you the convenience of exception propagation but with explicit error types and compiler-enforced handling.

This operator is called the ? operator.