Skip to Content
Course content

30: Panics vs Recoverable Errors

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a colleague who was building a high-throughput telemetry ingestor. He had used .unwrap() all over the place. His reasoning was simple: "The config file is managed by our deployment script, so it's impossible for the 'port' field to be missing or malformed." He felt that checking for an error was just adding boilerplate to code that would never actually fail.

Two weeks later, a junior SRE accidentally pushed a config change with a trailing whitespace character in the port number. The ingestor didn't just fail to start; it panicked and crashed the entire pod in a tight loop, triggering a cascade of alerts across the team at 3 AM. That's the danger of confusing a "should never happen" scenario with a "can't happen" scenario. In Rust, knowing when to let the program crash and when to handle the error gracefully is the difference between a resilient system and a fragile one.

When to Pull the Fire Alarm

A panic is essentially Rust's way of saying, "I have encountered a state that I cannot possibly recover from, and continuing would be dangerous." When a program panics, it starts unwinding the stack, cleaning up memory, and then terminates. It's a hard stop.

You'll encounter panics most often through the panic! macro, or more commonly, by calling .unwrap() or .expect("message") on a Option or Result. I generally advise against .unwrap() in production code unless you can prove—mathematically or logically—that the value is always there. For example, if you just checked if list.is_empty(), then calling list[0] is logically safe, though .first() is still more idiomatic.

fn get_critical_system_id() -> String {
    // If the environment variable is missing, the app literally cannot function.
    // This is a valid use of expect() because the app should not start without this.
    std::env::var("SYSTEM_ID").expect("SYSTEM_ID environment variable must be set")
}

Panics are for bugs. They are for invariant violations. If your code reaches a branch that is logically impossible to hit, panic! is your friend because it alerts you to a flaw in your logic immediately rather than letting the program limp along in a corrupted state.

Handling the Expected Chaos

Most errors aren't bugs; they're just part of the environment. A file is missing, a network socket timed out, or a user typed "apple" when you asked for a number. These are recoverable errors, and in Rust, we handle these using the Result<T, E> enum.

Instead of crashing, a function returns a Result, forcing the caller to decide how to handle the failure. This is where the ? operator becomes your best friend. It allows you to propagate the error up the call stack without writing a dozen nested match statements. I've seen developers fight the Result type at first, thinking it's too verbose, but once you've spent a weekend debugging a NullPointerException in Java or a segfault in C++, you'll realize that being forced to acknowledge the error is a gift.

fn parse_port(input: &str) -> Result<u16, std::num::ParseIntError> {
    let port: u16 = input.trim().parse()?; 
    Ok(port)
}

fn initialize_server(config_port: &str) -> Result<(), String> {
    let port = parse_port(config_port)
        .map_err(|e| format!("Invalid port number: {}", e))?;
    
    println!("Server starting on port {}", port);
    Ok(())
}

In the example above, parse_port doesn't decide how to handle a bad string; it just reports it. The initialize_server function then converts that low-level parsing error into a human-readable string. The program keeps running, the user gets a helpful message, and nobody gets woken up at 3 AM.

Choosing the Right Tool for the Job

The rule of thumb I use is this: if the error is caused by something outside your control (user input, filesystem, network), use Result. If the error is caused by a programmer's mistake (indexing out of bounds, calling a function in the wrong state), let it panic.

Ask yourself: "If this fails, can the program reasonably keep doing something else?" If the answer is yes—even if that "something else" is just printing an error and exiting cleanly—use Result. If the answer is "No, the internal state of my application is now totally invalid and I might corrupt data if I continue," then a panic is the most honest response your code can give.




📋 Practical Task

Building a Robust Configuration Validator

You are building a configuration loader for a database client. The client needs a timeout (in seconds) and a retry_limit. The timeout must be between 1 and 60 seconds. The retry_limit must be between 0 and 10.

Your Task: Implement a function validate_config(timeout_raw: &str, retry_raw: &str) -> Result<Config, String> that performs the following:

  • Parses the strings into integers. If parsing fails, return a Result::Err explaining which field was malformed.
  • Checks if the values are within the allowed ranges. If they are out of range, return a Result::Err.
  • Returns a Config struct wrapped in Ok if everything is valid.

The Twist: Create a second function called load_critical_config(). This function should call validate_config, but since this is a "critical" boot sequence, it should use .expect() to panic if the configuration is invalid, as the application cannot possibly function without these specific settings.

Requirements:
1. Define a Config struct with timeout: u32 and retry_limit: u32.
2. Use the ? operator inside validate_config for the parsing step.
3. Ensure your error messages are descriptive (e.g., "Timeout must be between 1 and 60").

Rating
0 0

There are no comments for now.

to be the first to leave a comment.