17–18. panic! and Unrecoverable Errors · Custom Error Types
panic is Rust's way of saying "this is unrecoverable, stop now." Array bounds, integer division by zero, and `.unwrap()` on None all trigger panic automatically. This lesson covers how panic works, unwind vs. abort, and when to invoke panic on purpose. Production libraries and services define concrete error enums so callers can branch on category — rather than rely on Box<dyn Error>. This lesson defines an error enum from scratch, implements Display, the Error trait, and From by hand, then collapses all of it with the thiserror crate.
What you'll learn
- 1List situations that automatically panic
- 2Trigger an explicit panic with the `panic!` macro
- 3Distinguish unwind vs. abort
- 4Use assert! / assert_eq! / debug_assert!
- 5Choose between Result-handling and panic
- 6Define an enum of domain-specific error variants
- 7Implement Display and Error by hand
- 8Wire From<T> for automatic conversion
- 9Use thiserror to cut the boilerplate to one line
- 10Tell apart anyhow and thiserror use cases
17. panic! and Unrecoverable Errors
panic is Rust's way of saying "this is unrecoverable, stop now." Array bounds, integer division by zero, and `.unwrap()` on None all trigger panic automatically. This lesson covers how panic works, unwind vs. abort, and when to invoke panic on purpose.
Overview
Panic resembles an unchecked exception in other languages, but Rust's messages are clean and stack traces are tidy. Most panics are **safety nets that surface bugs immediately** — debug-build integer overflow, .unwrap() on None, etc.
Core Concepts
1) Situations that auto-panic
- Array / slice index out of bounds
- Integer overflow (debug builds)
- Integer division by zero
- Option::unwrap() on None, Result::unwrap() on Err
- RefCell borrow-rule violations at runtime
2) Explicit panic
panic!("intentional stop — TODO: implement");
assert!(n > 0, "n must be positive");
assert_eq!(actual, expected, "values differ");3) unwind vs. abort
| Strategy | Behavior | Default |
|---|---|---|
| unwind | Runs Drops, releases resources | Dev/release default |
| abort | Exit immediately, no Drop | Opt-in via Cargo.toml panic = "abort" (smaller binaries) |
4) When to panic vs. return Result
- **panic** — unrecoverable bugs / invariant violations (continuing would be worse)
- **Result** — user input / external I/O / things that legitimately fail
Hands-on Examples
fn divide(a: i32, b: i32) -> i32 {
assert!(b != 0, "cannot divide by zero");
a / b
}
fn main() {
println!("{}", divide(10, 2)); // 5
println!("{}", divide(10, 0)); // thread 'main' panicked at 'cannot divide by zero'
}Debug-only assertion (stripped in release):
fn process(v: &[i32]) {
debug_assert!(!v.is_empty(), "empty slice passed");
// ... real processing
}Switch panic strategy via Cargo.toml:
[profile.release]
panic = "abort" # smaller binaries, no DropSee stack traces:
RUST_BACKTRACE=1 cargo run
# Detailed call stack up to the panic siteCommon Mistakes
Q. Is .unwrap() also a panic?
A. Yes — None/Err triggers panic. For user input or I/O prefer `?` or `.unwrap_or()`.
Q. Can I catch panics?
A. `std::panic::catch_unwind` exists but is not a general error-handling tool — it's for FFI boundaries or thread isolation. Day to day, use Result.
Q. debug_assert! gets stripped in release — can I rely on it?
A. It's a dev-time check. **For checks that must remain in release, use `assert!`** (always active).
Recap
- panic = unrecoverable, immediate stop
- Bounds / overflow / .unwrap on None trigger it automatically
- Explicit: panic! / assert! / assert_eq! macros
- panic for bugs / invariant violations, Result for legitimate failures
- RUST_BACKTRACE=1 for stack traces
Try It Yourself
- Intentionally trigger an out-of-bounds index and read the panic message + stack trace
- Write a simple test function with assert_eq! and verify it panics on a wrong value
- Switch release profile to abort and compare binary size
18. Custom Error Types (thiserror, From)
Production libraries and services define concrete error enums so callers can branch on category — rather than rely on Box<dyn Error>. This lesson defines an error enum from scratch, implements Display, the Error trait, and From by hand, then collapses all of it with the thiserror crate.
Overview
A library must tell its callers what **kind** of failure happened — `MyError::NotFound`, `MyError::PermissionDenied`, etc. — so the caller can branch. This lesson does it by hand once, then shows how thiserror reduces it to a one-liner per variant.
Core Concepts
1) Hand-rolled custom error
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound(String),
Invalid(String),
Io(std::io::Error),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound(s) => write!(f, "not found: {}", s),
AppError::Invalid(s) => write!(f, "invalid: {}", s),
AppError::Io(e) => write!(f, "io: {}", e),
}
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}2) thiserror — one-line equivalents
[dependencies]
thiserror = "1"use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("invalid: {0}")]
Invalid(String),
#[error("io: {0}")]
Io(#[from] std::io::Error), // auto-derive From
}3) thiserror vs. anyhow
| Crate | Role | Use case |
|---|---|---|
| thiserror | Library-grade concrete error enums via macro | Reusable libraries |
| anyhow | App-grade Box<dyn Error> wrapper + context | Binaries / services |
Hands-on Examples
thiserror plus `?` auto-conversion:
use thiserror::Error;
use std::fs;
use std::num::ParseIntError;
#[derive(Debug, Error)]
enum AppError {
#[error("file: {0}")]
Io(#[from] std::io::Error),
#[error("parse: {0}")]
Parse(#[from] ParseIntError),
#[error("empty file")]
Empty,
}
fn first_number(path: &str) -> Result<i32, AppError> {
let s = fs::read_to_string(path)?; // io::Error → AppError
let line = s.lines().next().ok_or(AppError::Empty)?;
Ok(line.trim().parse::<i32>()?) // ParseIntError → AppError
}
fn main() -> Result<(), AppError> {
println!("{}", first_number("data.txt")?);
Ok(())
}Branch on variants:
match first_number("data.txt") {
Ok(n) => println!("ok: {}", n),
Err(AppError::Empty) => eprintln!("file was empty"),
Err(AppError::Io(e)) => eprintln!("I/O failed: {}", e),
Err(AppError::Parse(e)) => eprintln!("not a number: {}", e),
}Common Mistakes
Q. Can I just always use Box<dyn Error>?
A. Fine for prototypes / main. In a library it deprives callers of variant-level branching — they have to downcast every time, which is brittle.
Q. thiserror and anyhow together?
A. Common combo. Library modules define concrete errors with thiserror; the binary that uses them returns `anyhow::Result` and attaches `.context(...)` strings.
Q. Do I really need to implement Error?
A. Required to interop with other libraries — you need it to fit into `Box<dyn Error>` and to follow the `source()` chain.
Recap
- Enum variants encode error categories so callers can branch
- Display / Error / From by hand, or thiserror in one line
- Libraries = thiserror, applications = anyhow (often mixed)
- `?` relies on the From trait for auto-conversion
Try It Yourself
- Hand-write a custom error for file I/O + integer parsing (Display, Error, From)
- Rewrite the same with thiserror and compare line counts
- Add `#[from]` to a variant and verify `?` auto-converts the source error
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗Comments 0
No comments yet. Be the first.
Only members can comment.
Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy