This episode breaks down error handling in Rust: the Result and Option types as error-safe values, match for handling results, the ? operator for concise error propagation, as well as building custom errors with thiserror and anyhow for production applications.

Almost every program in the real world can fail: a file is not found, a connection drops, or data is malformed. In other languages, errors are often handled through exceptions that jump out of the normal flow. Rust takes a different approach: errors are values, represented with the Result and Option types.
Episode 4 breaks down both types, the match pattern for handling them, the ? operator for concise error propagation, and the two most popular crates for custom errors: thiserror for libraries and anyhow for applications. After this episode, you will read Rust error messages with a clear head.
Result<T, E> expresses an operation that can succeed or fail: the Ok(T) variant carries the success value, the Err(E) variant carries the error. Option<T> expresses a value that may not exist: Some(T) or None. Both are ordinary enums — no magic, only type discipline.
cat > src/main.rs <<'EOF'
use std::fs::File;
fn main() {
let hasil = File::open("tidak-ada.txt");
match hasil {
Ok(file) => println!("file terbuka: {:?}", file),
Err(e) => println!("gagal membuka file: {}", e),
}
}
EOF
cargo runFile::open returns a Result<File, io::Error>. match hasil forces you to handle both cases. This is where Rust's power lies: the possibility of failure cannot simply be ignored.
Option answers the question "does the value exist?". The most common example: looking up an element in a collection.
cat > src/main.rs <<'EOF'
fn main() {
let angka = vec![10, 20, 30];
let pertama = angka.first();
let kosong: Vec<i32> = Vec::new();
let dari_kosong = kosong.first();
println!("pertama: {:?}", pertama);
println!("dari_kosong: {:?}", dari_kosong);
}
EOF
cargo runvec.first() returns an Option<&i32>: Some if an element exists, None if the vector is empty. By combining Option and Result, Rust handles the absence of values without null.
The match pattern from episode 3 is the basic way to handle Result and Option. For logic that only cares about the success case, combinators like unwrap_or, map, and ok_or make the code more compact:
cat > src/main.rs <<'EOF'
fn main() {
let input = "42";
let angka: Result<i32, _> = input.parse();
let nilai = angka.unwrap_or(0);
let opsional = Some(7);
let tambah = opsional.map(|n| n + 1);
println!("nilai: {}, tambah: {:?}", nilai, tambah);
}
EOF
cargo rununwrap_or(0) uses a fallback value if Err. map transforms the contents of Some without touching None. Avoid unwrap and expect in production code because both trigger a panic on Err or None.
Writing a match for every fallible call makes code messy. The ? operator shortens it: if Err, the function returns that error directly; if Ok, its contents are passed through to the variable.
cat > src/main.rs <<'EOF'
use std::fs;
use std::io;
fn baca_panjang(path: &str) -> Result<usize, io::Error> {
let konten = fs::read_to_string(path)?;
Ok(konten.len())
}
fn main() -> Result<(), io::Error> {
let panjang = baca_panjang("Cargo.toml")?;
println!("panjang Cargo.toml: {}", panjang);
Ok(())
}
EOF
cargo runfs::read_to_string(path)? returns the error to the caller on failure. Notice fn main() -> Result<(), io::Error>: main itself can now return an error, and the runtime prints it if it occurs.
The ? operator performs automatic conversion via the From trait: if the function returns io::Error but the source is ParseIntError, Rust calls the appropriate conversion. This is the foundation of an error ecosystem that connects together.
thiserror generates the Display and std::error::Error implementations from an enum definition — perfect for libraries that must provide structured errors:
cat > src/main.rs <<'EOF'
use std::fs;
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("file tidak ditemukan: {0}")]
NotFound(String),
#[error("gagal membaca: {0}")]
Io(#[from] std::io::Error),
}
fn baca(path: &str) -> Result<String, AppError> {
let konten = fs::read_to_string(path)?;
Ok(konten)
}
fn main() -> Result<(), AppError> {
let isi = baca("Cargo.toml")?;
println!("{}", isi.lines().next().unwrap_or_default());
Ok(())
}
EOF
cargo run#[error("file tidak ditemukan: {0}")] defines the message, and #[from] adds automatic conversion. cargo add thiserror adds the dependency to Cargo.toml. With #[from], the ? operator directly converts io::Error into AppError.
For binaries and applications that do not need structured errors, anyhow provides Result<T, anyhow::Error> with string context:
cat > src/main.rs <<'EOF'
use anyhow::{Context, Result};
fn main() -> Result<()> {
let data = std::fs::read_to_string("config.toml")
.context("gagal membaca config.toml")?;
println!("{}", data.lines().count());
Ok(())
}
EOF
cargo runcontext adds a contextual message to the error. The idiomatic combination in industry: thiserror for libraries, anyhow for applications and binaries.
Key takeaways:
Result<T, E> for operations that can fail; Option<T> for values that can be absent.match forces both cases to be handled; combinators like unwrap_or and map condense the logic.? operator propagates errors and converts types via the From trait.fn main() -> Result<(), E> lets errors bubble up to the runtime.thiserror for structured custom errors in libraries.anyhow with context for information-rich application errors.In the next episode 5 we will discuss struct, enum, trait, and generic — defining data with structs and tuple structs, enums as algebraic data types, traits as interfaces with default impls, as well as generic constraints for reusable code. You will start building data types that reflect the business domain.