Rust track

Lesson 15 · Part 4: Error Handling, Panics & Custom Errors

Result & Option

Result and Option types, unwrapping methods and the ? operator

Part 4: Error Handling, Panics & Custom Errors5 min readFree lesson

Error handling in Rust is done with values, not exceptions. Operations that can fail return Result<T, E>, and places where a value may be absent return Option<T>. In this article, we will review the key methods of these two types and summarize which handling to use in which situation. (The ? operator will be covered in detail in chapter 16.)

Learning Objectives

  • Understand the meaning and difference between Result<T, E> and Option<T>.
  • Besides match and if let, use chaining methods such as map, and_then, unwrap_or.
  • Handle Result, which is frequently encountered in the standard library, like parse::<i32>().
  • Develop a sense of using expect / unwrap only in safe places.

Key Concepts

1) Definitions of the Two Types

enum Option<T> { None, Some(T) }
enum Result<T, E> { Ok(T), Err(E) }
  • Option<T> — A place where a value may or may not exist.
  • Result<T, E> — A place where the result can be success or failure. Use this when you need to express the reason for failure.

2) The Most Common Handling — match / if let

match parsed {
    Ok(n) => println!("Success: {n}"),
    Err(e) => println!("Failure: {e}"),
}

3) Option Method Table

Method Operation Example
map(|x| ...) Transform the value if Some Some(2).map(|x| x*10) → Some(20)
and_then(|x| ...) Return another Option if Some Chaining
unwrap_or(default) Default value when None None.unwrap_or(-1) → -1
unwrap_or_else(|| ...) Calculated with a closure when None
ok_or(err) Option → Result Some(1).ok_or("None") → Ok(1)
is_some(), is_none() Presence check

4) Result Method Table

Method Behavior
map(|x| ...) Convert Ok value
map_err(|e| ...) Convert Err value — useful for error type conversion
ok() Result → Option (Err → None)
err() Err to Option
unwrap_or(default) Default value on Err
expect(msg) Panic with message on Err
? Propagate Err to caller (Part 16)

5) When to use unwrap and expect?

Use only in places where failure is clearly impossible — or where failure leading to program termination is acceptable. For user input, file I/O, and network results, ? or match is the correct choice.

It is good to leave a message in expect("This spot is always Ok") explaining why it is safe.

Examples

Example 1 — ex01_parse.rs: Handling parse results

//Example 1 of Chapter 15: Handling `Result` from `str.parse`
//
//`parse::<i32>()` returns `Result<i32, ParseIntError>`.
//The caller must explicitly handle success or failure.

fn main() {
    let inputs = ["42", "Error", "  7  ", "-3"];

    for s in inputs {
        //Trim whitespace before parsing
        let parsed = s.trim().parse::<i32>();

        match parsed {
            Ok(n) => println!("'{s}' → {n}"),
            Err(e) => println!("'{s}' → Failure: {e}"),
        }
    }

    //unwrap_or: default value on failure
    let n = "abc".parse::<i32>().unwrap_or(0);
    println!("unwrap_or result = {n}");
}

Example 2 — ex02_option_methods.rs: Option methods

//Example 2 of Episode 15: Collection of Option Methods
//
// map / and_then / unwrap_or / unwrap_or_else / ok_or

fn half(n: i32) -> Option<i32> {
    if n % 2 == 0 { Some(n / 2) } else { None }
}

fn main() {
    let v = Some(10);

    //map — Transform the value inside Some
    let plus_one = v.map(|x| x + 1);
    println!("map: {:?}", plus_one);

    //and_then — Return another Option when Some (chaining)
    let chained = v.and_then(half).and_then(half);
    println!("and_then twice: {:?}", chained);

    //unwrap_or — Default value when None
    let backup = None::<i32>.unwrap_or(-1);
    println!("unwrap_or(-1) = {backup}");

    //ok_or — Convert Option to Result (add error message)
    let r: Result<i32, &str> = Some(3).ok_or("No value");
    println!("ok_or = {:?}", r);
}

Example 3 — ex03_result_methods.rs: Result methods

//Example 3 of Chapter 15: Collection of Result Methods
//
// map / map_err / ok / err / expect

fn main() {
    let good: Result<i32, &str> = Ok(7);
    let bad: Result<i32, &str> = Err("Input format error");

    //map — Transform the value inside Ok (errors remain unchanged)
    println!("good.map(|x| x*2) = {:?}", good.map(|x| x * 2));
    println!("bad.map (|x| x*2) = {:?}", bad.map(|x| x * 2));

    //map_err — Transform the value inside Err
    let bad2: Result<i32, String> = bad.map_err(|e| format!("[Input error] {e}"));
    println!("map_err: {:?}", bad2);

    //ok — Result → Option (drop errors, convert to Some/None)
    let opt = good.ok();
    println!("good.ok() = {:?}", opt);

    //expect — Panic on Err, print a message together (only when safety is obvious)
    let safe = Ok::<i32, &str>(99).expect("This place is always Ok");
    println!("expect result = {safe}");
}

Common Mistakes

Q. What is the difference between unwrap_or and unwrap_or_else?

A. unwrap_or(default) precomputes the default value — even for Some/Ok, the default expression is evaluated. If default computation is expensive, use unwrap_or_else(|| expensive_expression) so that it evaluates only when None/Err.

Q. Which is better, match or map?

A. If you are simply "converting the value inside Ok/Some," one line of map is clean. If each branch has different side effects (e.g., output), match is clearer.

Q. What is the difference between ? and unwrap?

A. ? propagates to the caller on Err/None, while unwrap immediately panics. Library code almost always uses ?. In small beginner examples, it's natural to quickly test with unwrap, then switch to ? for production code.

Q. What should be written in expect messages?

A. Writing "why an Err cannot occur at this point" helps the next person understand. Example: .expect("Input cannot be empty — verified above").

Summary

  • Failure possibility is indicated in the type with Result<T, E>, absence possibility with Option<T>.
  • Conversion/chaining uses methods like map, and_then, map_err.
  • Recovery with default uses unwrap_or / unwrap_or_else.
  • unwrap/expect only in safe and obvious places; otherwise, use match/if let/?.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/result_option/Cargo.tomlTOML
[package]
name = "result_option"
version = "0.1.0"
edition = "2021"
default-run = "result_option"

[dependencies]
src/result_option/src/main.rsRust
fn main() {
    println!("This crate has three executable examples.");
    println!();
    println!("  cargo run --bin ex01_parse");
    println!("  cargo run --bin ex02_option_methods");
    println!("  cargo run --bin ex03_result_methods");
}

Assignment

Problem 1 — Safe parse

  • File name: homework_01/src/main.rs
  • Key concepts: Result, unwrap_or, map

Requirements

  • Define the function parse_or_default(s: &str) -> i32.
  • Parse the input as i32, and return 0 if it fails.
  • In main, print the results for the three inputs "42", "abc", " -7 ".

Expected output

42
0
-7

Hint

  • s.trim().parse::<i32>().unwrap_or(0).

Problem 2 — Option chaining

  • File name: homework_02/src/main.rs
  • Key concept: composing and_then functions that return Option

Requirements

  • Function half(n: i32) -> Option<i32> returns Some(n/2) if even, None if odd.
  • Function triple(n: i32) -> Option<i32> always returns Some(n*3) (simply).
  • In main, handle the following two inputs.
  • Input 16 → half → triple → half chaining
  • Input 9 → half → triple → half (None in the first step)

Expected output

16 → Some(12)
9 → None

Hint

  • Some(16).and_then(half).and_then(triple).and_then(half).

Check your answers

Try solving it yourself first, then compare your answer with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key Points:
//- `parse` has a generalized result, so the type is specified with `::<i32>`.
//- On failure, recovery to a default value is a one-liner with `unwrap_or(default)`.

fn parse_or_default(s: &str) -> i32 {
    s.trim().parse::<i32>().unwrap_or(0)
}

fn main() {
    for s in ["42", "abc", "  -7  "] {
        println!("{}", parse_or_default(s));
    }
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key Points:
//- `and_then` is chaining that calls the next function if it is Some, and returns None as is if it is None.
//- If even one step becomes None, the subsequent steps automatically propagate None.

fn half(n: i32) -> Option<i32> {
    if n % 2 == 0 { Some(n / 2) } else { None }
}

fn triple(n: i32) -> Option<i32> {
    Some(n * 3)
}

fn pipeline(n: i32) -> Option<i32> {
    Some(n).and_then(half).and_then(triple).and_then(half)
}

fn main() {
    for n in [16, 9] {
        println!("{n} → {:?}", pipeline(n));
    }
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now