What Is Error Handling In Rust?

Error handling means writing code that can detect, manage, and respond to mistakes or unexpected situations without crashing your program.

Rust provides two primary mechanisms for error handling:

  • panic! → It is used for unrecoverable errors, such as corrupted data or impossible states where the program must stop.
  • Result/Option → It works as a recoverable error, expected failures like a missing file, invalid input, or failed network request that your program can handle and recover from.

Why Is Error Handling Important?

  • Prevent Program Crashes: Without error handling, a single small error (like trying to open a missing file) could crash the whole program. With proper handling, you can show a message or try an alternative instead of crashing.
  • Improve Code Reliability: If your code expects errors and deals with them, it becomes more predictable, stable, and easier to maintain.
  • Ensure Data Safety: Handling errors properly ensures your program doesn’t corrupt data or leave resources (like files or memory) in a bad state.

Simple example – Without Error Handling

fn main() {
let content = std::fs::read_to_string("data.txt").unwrap(); // panic if file doesn't exist
println!("{}", content);
}

Better Way – With Error Handling

fn main() {
let result = std::fs::read_to_string("data.txt");

match result {
Ok(content) => println!("File content: {}", content),
Err(e) => println!("Could not read the file: {}", e),
}
}

Types of Errors in Rust

  1. Recoverable Errors
  2. Unrecoverable Errors

1) Recoverable Errors with Result Enum

In Rust, a recoverable error is a situation where something can go wrong, but your program can continue if you handle it properly. Rust gives us the Result enum to handle these types of errors:

enum Result<T, E> {
Ok(T), // Operation successful, holds the success value
Err(E), // Something went wrong, holds the error info
}
  • Ok(T): Indicates a successful operation, holding a value of type T.
  • Err(E): Represents an error, holding a value of type E.

How It Works: Handling File Operations

use std::fs::File;

fn main() {
// Try to open the file
let result = File::open("my_data.txt");

match result {
Ok(file) => {
println!("File opened successfully: {:?}", file);
}
Err(error) => {
println!("Could not open the file: {:?}", error);
println!("You can create the file before running the program again.");
}
}
}

Using .unwrap() and .expect() Function

If you’re sure the operation will succeed, you can use these shortcuts:

  • unwrap(): Extracts the value or panics if it’s an error.
  • expect(): Similar to unwrap(), but allows you to specify a custom error message.

1) unwrap() code example:

use std::fs::File;

fn main() {
let file = File::open("my_data.txt").unwrap();
println!("File opened: {:?}", file);
}

// It will return the file if successful, otherwise return the error

2) expect() code example:

use std::fs::File;

fn main() {
let file = File::open("my_data.txt")
.expect("Could not open the file. Make sure 'my_data.txt' exists!");
println!("File opened: {:?}", file);
}

// It same as unwrap(), but shows your custom message if it fails

How To Use Handling Errors with ? Operator?

The ? operator is a shortcut in Rust for handling errors with Result. It simplifies error propagation and automatically returns the error if one occurs, instead of writing long match or if let code for every operation.

Example:

use std::fs::File;
use std::io::{self, Read};

fn read_text(path: &str) -> Result<String, io::Error> {
// Try to open the file — if it fails, return the error immediately
let mut file = File::open(path)?;

// Prepare a String to hold the file content
let mut text = String::new();

// Try reading the content — again, if it fails, return the error
file.read_to_string(&mut text)?;

// If everything worked, return the text inside Ok()
Ok(text)
}

fn main() {
match read_text("my_notes.txt") {
Ok(content) => println!("File content:\n{}", content),
Err(err) => println!("Failed to read the file: {:?}", err),
}
}

In this code:

  • The function returns a Result.
  • If successful → Ok(String) (file content)
  • If error → Err(io::Error) (error info)

2) Unrecoverable Errors with panic!

In Rust, panic! is used for unrecoverable errors, these are mistakes so serious that the program cannot continue safely, so it immediately stops (crashes).

Example: Panic from Invalid Index Access

fn main() {
let fruits = vec!["Apple", "Mango", "Banana"];

// Trying to access index 5 which doesn't exist
println!("Selected fruit: {}", fruits[5]);

println!("This line will never run!"); // This won't execute
}

//fruits has only 3 items so accessing fruits[5] is invalid.

Output:

thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 5', src/main.rs:5:39

Example: Manually Causing a Panic

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero is not allowed!");
}
a / b
}

fn main() {
let result = divide(20, 0); // This will panic and stop the program
println!("Result: {}", result); // This will never run
}

// We checked if b is 0, we panic because dividing by zero is mathematically invalid.

Output:

thread 'main' panicked at 'Division by zero is not allowed!', src/main.rs:4:9

How To Use Custom Error Handling?

Sometimes, the built-in errors like Result or panic! There are not enough for your program. You want to define your own error types that explain exactly what went wrong in your app.

You can create your own error types by implementing the std: :fmt: :Debug or std: :fmt: :Display traits.

Example:

use std::fmt;

// Define custom error types
#[derive(Debug)]
enum FileError {
NotFound,
NoPermission,
InvalidFormat,
}

// Implement Display for human-readable messages
impl fmt::Display for FileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileError::NotFound => write!(f, "File not found on the system"),
FileError::NoPermission => write!(f, "You don't have permission to access this file"),
FileError::InvalidFormat => write!(f, "The file format is invalid"),
}
}
}

// Use the custom error
fn open_file(path: &str) -> Result<(), FileError> {
if path == "" {
return Err(FileError::NotFound);
} else if path == "secret.txt" {
return Err(FileError::NoPermission);
} else if !path.ends_with(".txt") {
return Err(FileError::InvalidFormat);
}
Ok(())
}

fn main() {
match open_file("data.csv") {
Ok(_) => println!("File opened successfully!"),
Err(e) => println!("Error occurred: {}", e),
}
}

Combining Result with Option

Sometimes, you may want to combine Result and Option to represent multiple levels of errors.

Example:

// Function to find a student's score
fn find_score(scores: Vec<i32>, target: i32) -> Option<Result<i32, String>> {
// No scores at all
if scores.is_empty() {
return None;
}

// Search for the target score
for &score in &scores {
if score == target {
return Some(Ok(score)); // Found
}
}

// Scores exist but target not found
Some(Err(format!("Score {} not found", target)))
}

fn main() {
let scores = vec![50, 60, 70];

match find_score(scores, 80) {
None => println!("No scores available to search."),
Some(Ok(found)) => println!("Score found: {}", found),
Some(Err(err)) => println!("Error: {}", err),
}
}

Third-Party Libraries for Error Handling

when projects grow larger, writing custom error types and managing all possible error cases manually can become tedious and repetitive.

These reasons Rust has third-party crates like thiserror and anyhow, which make error handling cleaner, shorter, and more readable.

1) thiserror:

The thiserror crate helps you create custom error types easily without writing too much boilerplate code. For example:

use thiserror::Error;

#[derive(Debug, Error)]
enum FileOperationError {
#[error("Unable to open file: {0}")]
OpenError(String),

#[error("File format is invalid")]
FormatError,
}

fn open_config(file: &str) -> Result<String, FileOperationError> {
if file.is_empty() {
return Err(FileOperationError::OpenError("File path is empty".to_string()));
}

// Simulating a format error
if file.ends_with(".txt") {
return Err(FileOperationError::FormatError);
}

Ok("File opened successfully!".to_string())
}

fn main() {
match open_config("config.txt") {
Ok(message) => println!("{}", message),
Err(e) => println!("Error occurred: {}", e),
}
}

Output:

Error occurred: File format is invalid

2) anyhow:

The anyhow crate is perfect for applications (not libraries) where you don’t need custom error types but want easy and flexible error propagation.

use anyhow::{Context, Result};
use std::fs;

fn read_config() -> Result<String> {
let content = fs::read_to_string("config.json")
.context("Failed to read the configuration file")?;
Ok(content)
}

fn main() -> Result<()> {
match read_config() {
Ok(data) => println!("Config Loaded:\n{}", data),
Err(err) => println!("Error: {}", err),
}
Ok(())
}

Output:

Error: Failed to read the configuration file
Caused by: No such file or directory (os error 2)

Learn More About Rust Programming

Exercise: Safe Temperature Reader

Create a Rust program that reads a temperature value (in Celsius) from a text file named temperature.txt.

  • If the file doesn’t exist, return a custom error.
  • If the file exists but is empty, return None.
  • If the file contains invalid data (e.g., not a number), return an error.
  • If everything is fine, return the temperature as a f64.
  • Use Result and Option properly and handle errors gracefully in main().

Starter Template (Students must complete the missing parts):

use std::fs;
use std::num::ParseFloatError;
use std::fmt;

#[derive(Debug)]
enum TempError {
FileMissing,
ParseError,
}

impl fmt::Display for TempError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TempError::FileMissing => write!(f, "File not found."),
TempError::ParseError => write!(f, "Failed to parse temperature."),
}
}
}

fn read_temperature(file: &str) -> Result<Option<f64>, TempError> {
let content = fs::read_to_string(file).map_err(|_| TempError::FileMissing)?;

if content.trim().is_empty() {
return Ok(None);
}

let temperature: f64 = content.trim().parse::<f64>().map_err(|_| TempError::ParseError)?;
Ok(Some(temperature))
}

fn main() {
match read_temperature("temperature.txt") {
Ok(Some(temp)) => println!("Current temperature: {:.1}°C", temp),
Ok(None) => println!("No temperature data found."),
Err(e) => println!("Error: {}", e),
}
}
  • Complete this code program, and learn how to use ? and map_err().

Leave a Comment