[Rust Series Part 11] Error Handling Basics

한국어 버전

Rust's take on error handling extends the language's philosophy of letting the compiler catch everything it can. In this chapter we look at what [[panic|panic!]], [[result|Result<T, E>]], and the [[question-mark-operator|? operator]] each do, and when to lean on the right tool so production code stays predictable.

Why Error Handling Matters

  • Systems-language reality: When you deal with files, networks, or user input, failure is normal, so the failure path must be explicit.
  • Ownership integration: To clean up resources even when something goes wrong, the error story needs to live inside the type system.
  • Minimize runtime panics: Because panic! aborts the current thread immediately, recoverable situations should express failure via Result instead.

Stopping Execution with [[panic|panic!]]

Use [[panic|panic!]] when continuing past a certain point would be incorrect.

fn main() {
    let config = std::env::var("APP_CONFIG")
        .unwrap_or_else(|_| panic!("APP_CONFIG must be set"));

    println!("config = {}", config);
}
  • A panic! unwinds the stack and terminates the current thread.
  • It's handy while debugging "this should never happen" states or hunting for failure points in toy projects.
  • In long-running apps such as CLIs or servers, avoid overusing it and model recoverable failures with Result.

Treating Failure as Data with [[result|Result<T, E>]]

[[result|Result]] has the shape enum Result<T, E> { Ok(T), Err(E) }, so success and failure both become values.

fn read_config() -> Result<String, std::io::Error> {
    std::fs::read_to_string("./config.txt")
}

fn main() {
    match read_config() {
        Ok(content) => println!("config: {}", content),
        Err(err) => eprintln!("Failed to read file: {}", err),
    }
}
  • Ok(T) stores the success value, Err(E) stores why it failed.
  • The E type lets you describe failures precisely, and match or if let keep control flow explicit.
  • Returning Result forces callers to acknowledge failure up front, so just reading a signature hints at safety expectations.

Helper Methods Worth Knowing

  • unwrap, expect: Useful for quick prototypes when you need a T immediately. For production, emit meaningful messages or replace them with proper handling.
  • map, map_err, and_then: Apply functions to the success or error side in a functional style.

Propagating Errors with [[question-mark-operator|?]]

The [[question-mark-operator|? operator]] short-circuits when it sees an Err (or None) and returns early from the current function.

fn load_user(id: u64) -> Result<String, Box<dyn std::error::Error>> {
    let path = format!("./users/{}.json", id);
    let raw = std::fs::read_to_string(&path)?;
    let parsed: serde_json::Value = serde_json::from_str(&raw)?;
    Ok(parsed["name"].as_str().unwrap_or("unknown").to_string())
}
  • ? forwards the Err up the call stack, matching the function's return type. When it sees Ok, it unwraps the inner value and keeps going.
  • The current function must also return a [[result|Result]] or [[option|Option]], and the error type must be compatible with what you propagate.
  • Using ? removes nested match blocks and keeps the happy path straight.

Choosing the Right Tool

Situation Recommended tool Reason
Irrecoverable bug, debug guardrail panic!, unreachable! Surface the bug immediately so you can fix it fast
File/network operations or other expected failures Result Preserve the failure details and hand control to the caller
Short tutorials or prototypes unwrap, expect Keep samples compact, then swap in safer handling later
Unifying many error sources Result<T, Box<dyn Error>> or a custom enum Report multiple error kinds through one channel

When in doubt during the early stages, remember this cheat sheet:

  • Learning snippets: allow unwrap/expect briefly.
  • Naturally fallible work: return a Result.
  • State that must not continue: panic! is acceptable.

Practice in CodeSandbox

The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. Fork the starter and open src/main.rs
  2. Paste the lesson code and run cargo check plus cargo run in order
  3. Change types, values, or borrowing flow and compare the compiler feedback with the output

Rust practice here is mainly terminal-driven rather than browser-preview driven. Lessons that need multiple files or extra crates may require a bit more setup inside the starter.

Practice Exercises

  1. Combine std::fs::File::open and read_to_string into a helper function and refactor it to use ?.
  2. Write a function that calls panic! only when an environment variable is missing, otherwise returning a Result.
  3. For every expect, include why a panic is acceptable there so logs reveal the intent later.

Completion Checklist

  • Can state the difference between panic! and Result in one sentence.
  • Have written a function that chains several Results with ?.
  • Have your own rule for when unwrap/expect is acceptable.

Next time we'll organize modules and packages so that error paths flow along clear boundaries as your project grows.

💬 댓글

이 글에 대한 의견을 남겨주세요