Rust track

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

Panics

Raising panics, backtraces and panic hooks

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

Rust errors are divided into two types. Errors that the caller can handle meaningfully, recoverable errors, are represented by Result<T, E>, while situations where "something went wrong — do not proceed further," unrecoverable errors, are handled with panic!. In this chapter, we summarize the operation and debugging tips of panic!, index OOB, and unwrap/expect.

Learning Objectives

  • Understand the meaning and termination method of the panic! macro.
  • Know common causes of panics such as index OOB, integer overflow, unwrap, etc.
  • Develop the habit of increasing debugging possibilities using expect("Reason") messages.
  • Check the stack trace with RUST_BACKTRACE=1.

Key Concepts

1) Operation of panic!

When panic!("Message") is called, the basic operation is as follows:

  1. Print the error message and location to the console.
  2. Unwind the current thread's stack and drop all local variables drop.
  3. If the thread is the main thread, the process terminates (exit code ≠ 0).

If panic = "abort" is used in [profile] of Cargo.toml, it terminates immediately without unwinding, reducing the binary size.

2) Common causes of panic

Cause Example Safe alternative
Index OOB v[10] but length 3 v.get(10) (Option)
Integer overflow (debug) u8 + u8 checked_add, wrapping_add
unwrap/expect None.unwrap() match/?/unwrap_or
Division by 0 (integer) n / 0 Pre-check or checked_div

3) unwrap vs expect

let n: i32 = "42".parse().unwrap();           //General message on failure
let n: i32 = "42".parse().expect("Input must always be an integer"); //Our message on failure

It is easier for the next person to understand if you write the message for expect as "Why must this not fail here."

4) Viewing Backtrace

RUST_BACKTRACE=1 cargo run --bin ex01_explicit
RUST_BACKTRACE=full cargo run --bin ex01_explicit

You can see the entire call stack, which is very helpful for debugging.

5) Recoverable vs Non-recoverable — How to Choose?

Situation Handling
User input · external data · I/O Result + ? (recoverable)
Internal library integrity check (invariant violation) panic!
Obviously unreachable branch unreachable!()
Branch not yet implemented todo!()

Library code almost always returns Result as a polite manner. Leave the decision to the caller.

Examples

These examples catch panic with catch_unwind for learning purposes to produce clean output. In real code, catch_unwind is rarely used — usually just let it die.

Example 1 — ex01_explicit.rs: calling panic! directly

//Episode 17 Example 1: Direct call to panic!
//
//When you encounter an "irrecoverable" situation, use panic!("message") to immediately terminate the program.
//This example wraps it in catch_unwind to show both normal flow and panic flow.
//(In real code, catch_unwind is rarely used — usually you just let it die.)

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

fn main() {
    //Turning off the default panic hook makes the stack trace invisible and keeps the console clean.
    std::panic::set_hook(Box::new(|_| {}));

    println!("Normal: 10/2 = {}", divide(10, 2));

    //Catch a panic and extract the message (for learning purposes)
    let result = std::panic::catch_unwind(|| divide(10, 0));
    if let Err(payload) = result {
        let msg = payload
            .downcast_ref::<String>()
            .cloned()
            .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
            .unwrap_or_else(|| String::from("(Failed to extract message)"));
        println!("Panic occurred! Message: {msg}");
    }
}

Example 2 — ex02_index_oob.rs: Index OOB

//Example 17-2: Index Out of Bounds Panic
//
//Accessing an index out of bounds in arrays or vectors is not caught by the compiler and panics at runtime.
//A safe alternative is `.get(i)` — it returns the result as an Option.

fn main() {
    std::panic::set_hook(Box::new(|_| {}));

    let v = vec![10, 20, 30];
    println!("v[0] = {}, v[2] = {}", v[0], v[2]);

    //v[10] is an OOB index → panic
    let r = std::panic::catch_unwind(|| v.clone()[10]);
    if r.is_err() {
        println!("Accessing v[10] causes a panic — because it is out of bounds.");
    }

    //Safe alternative: receive as Option
    println!("v.get(2)  = {:?}", v.get(2));
    println!("v.get(10) = {:?}", v.get(10));
}

Example 3 — ex03_expect.rs : expect message

//Example 3 of Lesson 17: expect and RUST_BACKTRACE
//
//`unwrap` only displays the general message "panicked if there is no value."
//`expect("reason")` prints a message along with the panic, which is very helpful for debugging.
//Also, if you run with the environment variable RUST_BACKTRACE=1, you can see the call stack.

fn main() {
    std::panic::set_hook(Box::new(|_| {}));

    //Using expect in a safe place — the message becomes documentation of "why it is safe."
    let s = "42";
    let n: i32 = s.parse().expect("This place is always an integer — verified above");
    println!("n = {n}");

    //Check the expect message on failure
    let r = std::panic::catch_unwind(|| {
        let _: i32 = "abc".parse().expect("Input must always be an integer");
    });
    if r.is_err() {
        println!("expect panic occurs — message is very helpful for debugging.");
    }

    println!("If you run with the environment variable RUST_BACKTRACE=1, you can check the stack trace.");
}

Common Mistakes

Q. Can a panic be caught with catch?

A. It can be caught with std::panic::catch_unwind, but this is not a general exception handling mechanism. It is meant for special purposes, such as preventing FFI boundaries or background tasks from killing the main thread. For regular code, use Result and ?.

Q. If panic! occurs, do other threads also die?

A. Only the thread where the panic occurs dies. If the main thread dies, the entire process ends. This will be revisited in the thread lesson in Part 21.

Q. Is integer overflow always a panic?

A. It panics in debug builds and wraps in release builds. If wrapping is intended, use wrapping_add; if detection is needed, explicitly use checked_add (Option) / overflowing_add (tuple).

Q. Is it okay to use panic in a library?

A. It is allowed when the caller is in an unrecoverable situation (such as invariant violation), but panicking for user input or external data is very rude. Usually, the library returns information via Result and lets the user decide.

Summary

  • panic! is the last resort for unrecoverable situations — thread termination + drop.
  • Common causes include index OOB, integer overflow (debug), and unwrap.
  • Leaving a message with expect("Reason") instead of unwrap makes debugging easier.
  • The library almost always returns Result — the decision is up to the caller.

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/panic/Cargo.tomlTOML
[package]
name = "panic"
version = "0.1.0"
edition = "2021"
default-run = "panic"

[dependencies]
src/panic/src/main.rsRust
fn main() {
    println!("This crate has three executable examples.");
    println!();
    println!("  cargo run --bin ex01_explicit");
    println!("  cargo run --bin ex02_index_oob");
    println!("  cargo run --bin ex03_expect");
}

Assignment

Problem 1 — Safe Index Access

  • File name: homework_01/src/main.rs
  • Key concepts: Vec::get, Option

Requirements

  • Implement the function nth(v: &[i32], i: usize) -> String.
  • If v.get(i) is Some(n) then "v[{i}] = {n}", if None then "v[{i}] is out of range".
  • In main, print the result of calling i = 0, 2, 5 on vec![10, 20, 30].

Expected output

v[0] = 10
v[2] = 30
v[5] is out of range

Hint

  • match v.get(i) { Some(n) => format!(...), None => format!(...) }.

Problem 2 — Using expect with Validated Input

  • File name: homework_02/src/main.rs
  • Key concept: use of expect("Reason")

Requirements

  • An array let inputs = ["1", "2", "3"]; is given.
  • We know all elements are integers — so parse them with expect("Verify that it is an integer above").
  • Sum them all and print.

Expected output

Total = 6

Hint

  • let sum: i32 = inputs.iter().map(|s| s.parse::<i32>().expect("...")).sum();

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:
//- `v[i]` panics on OOB, `v.get(i)` safely indicates None.
//- Handle "abnormal input" as normal output through Option branching.

fn nth(v: &[i32], i: usize) -> String {
    match v.get(i) {
        Some(n) => format!("v[{i}] = {n}"),
        None => format!("v[{i}] is out of range"),
    }
}

fn main() {
    let v = vec![10, 20, 30];
    for i in [0usize, 2, 5] {
        println!("{}", nth(&v, i));
    }
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key Points:
//- In places where the input is clearly safe, it is good to explain "why it is safe" using the message of expect.
//- The pattern iter().map(...).sum() appears very frequently.

fn main() {
    let inputs = ["1", "2", "3"];
    let sum: i32 = inputs
        .iter()
        .map(|s| s.parse::<i32>().expect("Verify that it is an integer above"))
        .sum();
    println!("Sum = {sum}");
}

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