15-rustTermsLevel_04unwrap() / expect()

unwrap() / expect()

Level 4 — Error Handling & Generics Extract the inner value or panic; use only when failure is truly unexpected.


1. Prerequisites

  • Result<T, E> — The success/error wrapper these methods act upon.
  • Option<T> — The some/none wrapper these methods can also act upon.
  • ? Operator — The safe, preferred alternative to these methods.

2. Term Category

Rust-specific (the necessary evil): Rust is famous for forcing you to safely handle every possible error. But sometimes, a human knows that an error is impossible, even if the compiler's math can't prove it. unwrap and expect exist as an explicit "escape hatch" for these exact situations.


3. Explanation

(1) Design Motivation — "Why did we design this?"

The compiler is incredibly strict. If you write "127.0.0.1".parse::<IpAddr>(), the compiler forces you to handle the Result because parse can fail (e.g., if you passed it "hello").

But you, the human, know that "127.0.0.1" is a perfectly valid IP address. It is mathematically impossible for this specific, hardcoded string to fail parsing. Writing a massive match statement or propagating an impossible error with ? feels tedious and misleading to other programmers.

To fix this, Rust provides .unwrap() and .expect(). These methods instantly tear open the Result or Option and give you the inner value!

However, they are extremely dangerous. If you were wrong, and the value actually was an error or None, they instantly Panic and crash your entire program.

(2) Reality Metaphor

Imagine receiving a locked safe (Result) that might contain a diamond (Ok), or might contain a bomb (Err).

Using the ? operator is like carefully calling the bomb squad. They inspect the safe, and if there is a bomb, they safely remove it and report the issue to you without anyone getting hurt (safe early return).

Using .unwrap() is taking a giant sledgehammer and blindly smashing the safe open. If there's a diamond inside, great! You get it instantly. If there's a bomb inside… you just blew up the entire building (your program crashed).

You should only use the sledgehammer if you are 100% absolutely certain there is a diamond inside the safe.

(3) Rust Code Examples

Short Snippet (The Valid Sledgehammer)

Because the IP address is hardcoded, it will never fail. Using unwrap() here is perfectly acceptable and idiomatic Rust.

use std::net::IpAddr;

fn main() {
    // We use .unwrap() to instantly get the IpAddr out of the Result
    let home: IpAddr = "127.0.0.1".parse().unwrap();
    
    println!("My IP is: {}", home);
}

Fuller Example (unwrap vs expect)

If you smash the safe and the program crashes, .unwrap() prints a very generic, ugly error message.

If you use .expect("msg"), it does the exact same thing as unwrap, but it prints your custom message right before it crashes! This helps your future self debug why it crashed.

fn main() {
    let bad_ip = "127.0.0.BOOM"; // This will fail to parse!
    
    // If we use unwrap(), the program crashes with a generic message:
    // "called `Result::unwrap()` on an `Err` value: AddrParseError(InvalidIPv4)"
    // let ip1: IpAddr = bad_ip.parse().unwrap(); 
    
    // If we use expect(), the program crashes with OUR message:
    // "CRITICAL BUG: The hardcoded IP was somehow corrupted!: AddrParseError(InvalidIPv4)"
    let ip2: std::net::IpAddr = bad_ip.parse().expect("CRITICAL BUG: The hardcoded IP was somehow corrupted!"); 
}

4. Common Mistakes & Pitfalls

Mistake 1: Misunderstanding Unwrap Expect Scoping and Lifecycle Rules

The mistake: Assuming Unwrap Expect instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.

Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.

Incorrect:

fn get_ref() -> &str {
    let s = String::from("unwrap_expect_data");
    &s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}

Fix:

fn get_string() -> String {
    let s = String::from("unwrap_expect_data");
    s // Ownership of the String is transferred directly to the caller
}

Mistake 2: Mutating Unwrap Expect State Without Exclusive Ownership or mut Borrowing

The mistake: Attempting to mutate data associated with Unwrap Expect through an immutable reference &T or without specifying mut in variable declarations.

Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.

Incorrect:

fn update_val(data: &i32) {
    // *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}

Fix:

fn update_val(data: &mut i32) {
    *data += 1; // Correct: exclusive mutable reference permits mutation
}

Mistake 3: Concurrent Access to Unwrap Expect Across Threads Without Send / Sync Guards

The mistake: Sharing non-thread-safe Unwrap Expect instances across OS threads via std::thread::spawn.

Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).

Incorrect:

use std::rc::Rc;
use std::thread;

let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely

Fix:

use std::sync::Arc;
use std::thread;

let arc = Arc::new(42);
thread::spawn(move || {
    println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});

5. Practice Exercises

Exercise 1: Microservice Bootstrapping & Invariant-Guaranteed Configuration Engine

Scenario: Problem Statement: In enterprise microservices, application startup relies on parsing both hardcoded system defaults (e.g., default loopback listener IP) and dynamic user configuration sources (e.g., environment variable maps). Using unannotated .unwrap() during initialization creates dangerous ambiguity: if static string parsing fails, diagnosing the build artifact failure is difficult without custom diagnostic context messages (.expect()). Conversely, runtime failures from missing user environment variables should use explicit non-panicking fallback strategies (.unwrap_or_else(), .unwrap_or()) or return domain-specific diagnostic errors.

Requirements: Implement a generic ConfigLoader trait and a ServerConfig bootstrap builder that parses raw environment values:

  1. Implement static constant/hardcoded parsing using .expect("INVARIANT_VIOLATION: ...") with precise error messages explaining why the hardcoded value must parse successfully.
  2. Implement dynamic environment value retrieval with .unwrap_or_else() to supply calculated defaults when optional configuration variables are omitted.
  3. Validate that valid configurations extract inner values properly while invalid or missing required variables trigger domain-specific error handling.
Answer

Implementation

use std::collections::HashMap;
use std::net::IpAddr;

#[derive(Debug, PartialEq, Eq)]
pub enum ConfigError {
    MissingKey(String),
    InvalidValue { key: String, reason: String },
}

pub trait ConfigLoader {
    fn load_env_var(&self, key: &str) -> Result<String, ConfigError>;
}

pub struct HashMapConfigLoader {
    env_vars: HashMap<String, String>,
}

impl HashMapConfigLoader {
    pub fn new(vars: Vec<(&str, &str)>) -> Self {
        let mut env_vars = HashMap::new();
        for (k, v) in vars {
            env_vars.insert(k.to_string(), v.to_string());
        }
        Self { env_vars }
    }
}

impl ConfigLoader for HashMapConfigLoader {
    fn load_env_var(&self, key: &str) -> Result<String, ConfigError> {
        self.env_vars
            .get(key)
            .cloned()
            .ok_or_else(|| ConfigError::MissingKey(key.to_string()))
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct ServerConfig {
    pub bind_address: IpAddr,
    pub port: u16,
    pub max_connections: usize,
    pub service_name: String,
}

impl ServerConfig {
    pub fn bootstrap<L: ConfigLoader>(loader: &L) -> Result<Self, ConfigError> {
        // 1. Hardcoded static default IP parsing:
        // Using .expect() here is valid because "127.0.0.1" is a hardcoded string literal.
        // If this fails, it indicates a catastrophic programmer error or corrupted binary.
        let bind_address: IpAddr = "127.0.0.1"
            .parse()
            .expect("INVARIANT_VIOLATION: Hardcoded loopback IP string '127.0.0.1' failed to parse");

        // 2. Dynamic environment configuration with fallback default using unwrap_or_else
        let port: u16 = loader
            .load_env_var("PORT")
            .map(|val| {
                val.parse::<u16>().map_err(|e| ConfigError::InvalidValue {
                    key: "PORT".to_string(),
                    reason: e.to_string(),
                })
            })
            .unwrap_or_else(|_| Ok(8080))?;

        // 3. Dynamic max connections configuration using unwrap_or
        let max_connections = loader
            .load_env_var("MAX_CONNECTIONS")
            .ok()
            .and_then(|val| val.parse::<usize>().ok())
            .unwrap_or(1000);

        // 4. Required configuration: Must not use unwrap/expect because user environment can omit it
        let service_name = loader.load_env_var("SERVICE_NAME")?;

        Ok(ServerConfig {
            bind_address,
            port,
            max_connections,
            service_name,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bootstrap_defaults() {
        let loader = HashMapConfigLoader::new(vec![("SERVICE_NAME", "auth_service")]);
        let config = ServerConfig::bootstrap(&loader).expect("Bootstrap should succeed with defaults");

        // Explicit assertions
        assert_eq!(config.port, 8080);
        assert_eq!(config.max_connections, 1000);
        assert_eq!(config.service_name, "auth_service");
        assert_ne!(config.port, 9090);
        assert!(config.bind_address.is_loopback());
    }

    #[test]
    fn test_bootstrap_custom_overrides() {
        let loader = HashMapConfigLoader::new(vec![
            ("SERVICE_NAME", "payment_service"),
            ("PORT", "9090"),
            ("MAX_CONNECTIONS", "5000"),
        ]);
        let config = ServerConfig::bootstrap(&loader).expect("Bootstrap should succeed with overrides");

        assert_eq!(config.port, 9090);
        assert_eq!(config.max_connections, 5000);
        assert_eq!(config.service_name, "payment_service");
    }

    #[test]
    fn test_bootstrap_missing_required_key() {
        let loader = HashMapConfigLoader::new(vec![]);
        let result = ServerConfig::bootstrap(&loader);

        assert!(result.is_err());
        assert!(matches!(result, Err(ConfigError::MissingKey(ref k)) if k == "SERVICE_NAME"));
    }
}

Technical Explanation

  1. Static vs. Dynamic Failure Domains: Hardcoded string literals like "127.0.0.1" are verified during code authoring. Calling .expect("INVARIANT_VIOLATION: ...") documents that failure represents a compiler/binary corruption bug rather than a user configuration error.
  2. Fallback Strategy Allocation: .unwrap_or_else(|_| Ok(8080)) evaluates the closure lazily only when the Result or Option is empty. In contrast, .unwrap_or() eagerly evaluates its default parameter, which could incur unnecessary allocation or compute costs for complex types.
  3. Error Propagation Boundaries: Required keys (like SERVICE_NAME) use the ? operator to return a ConfigError::MissingKey variant to the caller rather than crashing the process with .unwrap(). This cleanly separates fatal bootstrap defects from recoverable initialization errors.
  4. Memory & Lifetimes: String values extracted from environment maps transfer full ownership (String) into the ServerConfig struct, avoiding borrowed reference lifetimes (&str) across async task spawning boundaries.

Exercise 2: High-Throughput Fixed-Capacity Invariant Buffer with Safe Unwrapping

Scenario: Problem Statement: High-performance telemetry pipelines often utilize a fixed-capacity ring buffer (BoundedRingBuffer<T, const CAP: usize>) to avoid dynamic heap allocations on every write. When popping items, internal index invariants (count > 0) ensure that slots populated prior to incrementing tail contain Some(T). Direct indexing into an unvalidated slice risks out-of-bounds panics, whereas calling .take() on an Option<T> slot guarded by structural buffer invariants allows the buffer to extract the inner item using .expect("INVARIANT_VIOLATION: Ring buffer count > 0 but slot contained None").

Requirements: Implement a generic bounded ring buffer BoundedRingBuffer<T, const CAP: usize> that:

  1. Implements push(&mut self, item: T) -> Result<(), BufferError> returning an error if full.
  2. Implements pop(&mut self) -> Result<T, BufferError> using index arithmetic and .take().expect("...") on buffer slots, where .expect() documents the mathematical proof that the slot cannot be None.
  3. Implements a generic stream processor trait FrameProcessor<T> that operates on the buffer, demonstrating static dispatch via generics (fn process_buffer_stream<P: FrameProcessor<i32>, const CAP: usize>).
Answer

Implementation

#[derive(Debug, PartialEq, Eq)]
pub enum BufferError {
    Full,
    Empty,
}

pub struct BoundedRingBuffer<T, const CAP: usize> {
    slots: [Option<T>; CAP],
    head: usize,
    tail: usize,
    count: usize,
}

impl<T, const CAP: usize> BoundedRingBuffer<T, CAP> {
    pub fn new() -> Self {
        Self {
            slots: std::array::from_fn(|_| None),
            head: 0,
            tail: 0,
            count: 0,
        }
    }

    pub fn push(&mut self, item: T) -> Result<(), BufferError> {
        if self.count == CAP {
            return Err(BufferError::Full);
        }
        self.slots[self.tail] = Some(item);
        self.tail = (self.tail + 1) % CAP;
        self.count += 1;
        Ok(())
    }

    pub fn pop(&mut self) -> Result<T, BufferError> {
        if self.count == 0 {
            return Err(BufferError::Empty);
        }
        // INVARIANT GUARANTEE:
        // Because count > 0, self.slots[self.head] is mathematically guaranteed to be Some(T).
        // Using .expect() here explicitly documents this structural invariant.
        let item = self.slots[self.head]
            .take()
            .expect("INVARIANT_VIOLATION: Ring buffer count > 0 but slot contained None");
        self.head = (self.head + 1) % CAP;
        self.count -= 1;
        Ok(item)
    }

    pub fn len(&self) -> usize {
        self.count
    }

    pub fn is_empty(&self) -> bool {
        self.count == 0
    }
}

pub trait FrameProcessor<T> {
    fn process_frame(&mut self, frame: T) -> T;
}

pub struct DoublingProcessor;

impl FrameProcessor<i32> for DoublingProcessor {
    fn process_frame(&mut self, frame: i32) -> i32 {
        frame * 2
    }
}

// Static dispatch generic processor
pub fn process_buffer_stream<P: FrameProcessor<i32>, const CAP: usize>(
    buffer: &mut BoundedRingBuffer<i32, CAP>,
    processor: &mut P,
) -> Vec<i32> {
    let mut results = Vec::new();
    while let Ok(frame) = buffer.pop() {
        results.push(processor.process_frame(frame));
    }
    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ring_buffer_push_pop_invariants() {
        let mut buf = BoundedRingBuffer::<i32, 3>::new();
        assert!(buf.is_empty());
        assert_eq!(buf.len(), 0);

        assert!(buf.push(10).is_ok());
        assert!(buf.push(20).is_ok());
        assert!(buf.push(30).is_ok());

        assert_eq!(buf.len(), 3);
        assert!(matches!(buf.push(40), Err(BufferError::Full)));

        let item = buf.pop().expect("Pop must succeed when buffer is non-empty");
        assert_eq!(item, 10);
        assert_ne!(buf.len(), 3);
        assert_eq!(buf.len(), 2);
    }

    #[test]
    fn test_ring_buffer_wraparound() {
        let mut buf = BoundedRingBuffer::<i32, 2>::new();
        assert!(buf.push(1).is_ok());
        assert_eq!(buf.pop().unwrap(), 1);

        assert!(buf.push(2).is_ok());
        assert!(buf.push(3).is_ok());
        assert_eq!(buf.pop().unwrap(), 2);
        assert_eq!(buf.pop().unwrap(), 3);

        let empty_res = buf.pop();
        assert!(matches!(empty_res, Err(BufferError::Empty)));
    }

    #[test]
    fn test_static_dispatch_stream_processing() {
        let mut buf = BoundedRingBuffer::<i32, 4>::new();
        buf.push(5).unwrap();
        buf.push(15).unwrap();

        let mut proc = DoublingProcessor;
        let processed = process_buffer_stream(&mut buf, &mut proc);

        assert_eq!(processed, vec![10, 30]);
        assert!(buf.is_empty());
    }
}

Technical Explanation

  1. Structural Invariants & .expect(): The buffer maintains count, head, and tail invariants. When self.count > 0, slot self.slots[self.head] is mathematically guaranteed to be populated with Some(item). Utilizing .expect(...) explicitly documents this logical invariant for code maintainers while providing diagnostic output if internal buffer arithmetic were ever corrupted.
  2. Monomorphization & Const Generics: The buffer uses const generics const CAP: usize and generic type T. During compilation, Rust monomorphizes separate machine code for each unique (T, CAP) combination (e.g., BoundedRingBuffer<i32, 3> vs BoundedRingBuffer<i32, 4>). This eliminates dynamic memory allocations and enables zero-cost static dispatch in process_buffer_stream.
  3. Option State Extraction (.take()): .take() replaces slots[head] with None while extracting Some(T) without copying or cloning T. This preserves strict move semantics for non-Copy types.
  4. Edge Cases: Index wrap-around (self.head + 1) % CAP prevents index-out-of-bounds runtime panics while keeping array bounds checking efficient.

Exercise 3: Dynamic Microservice Plugin Registry with Dynamic Dispatch & Contract Enforcement

Scenario: Problem Statement: An enterprise API gateway handles HTTP requests through a pipeline of middleware filters (RequestFilter trait). The system registers mandatory core filters (e.g. AuthGuardFilter) and optional extension filters (e.g. RateLimitFilter). During application initialization, mandatory core filters are registered into a hash map indexed by filter name. During request processing, fetching a mandatory filter from the internal registry uses .expect("FATAL_BOOTSTRAP_ERROR: Mandatory plugin 'auth_guard' missing from registry") to enforce initialization invariants. Optional filters use .get() with safe if let handling.

Requirements: Implement:

  1. A trait RequestFilter: Send + Sync with fn filter_id(&self) -> &'static str and fn apply(&self, req: &mut RequestContext) -> Result<(), FilterError>.
  2. A GatewayPipeline using trait objects (Box<dyn RequestFilter>) for dynamic dispatch.
  3. A pipeline executor that fetches core filters with .expect(...) and optional filters gracefully, demonstrating static vs dynamic dispatch invariants and error boundaries.
Answer

Implementation

use std::collections::HashMap;

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct RequestContext {
    pub uri: String,
    pub headers: HashMap<String, String>,
    pub authenticated_user: Option<String>,
}

#[derive(Debug, PartialEq, Eq)]
pub enum FilterError {
    Unauthorized(String),
    BadRequest(String),
}

pub trait RequestFilter: Send + Sync {
    fn filter_id(&self) -> &'static str;
    fn apply(&self, req: &mut RequestContext) -> Result<(), FilterError>;
}

pub struct AuthGuardFilter;

impl RequestFilter for AuthGuardFilter {
    fn filter_id(&self) -> &'static str {
        "auth_guard"
    }

    fn apply(&self, req: &mut RequestContext) -> Result<(), FilterError> {
        if let Some(token) = req.headers.get("Authorization") {
            if token.starts_with("Bearer ") {
                req.authenticated_user = Some("user_123".to_string());
                return Ok(());
            }
        }
        Err(FilterError::Unauthorized("Missing or invalid bearer token".to_string()))
    }
}

pub struct RateLimitFilter {
    pub max_requests: u32,
}

impl RequestFilter for RateLimitFilter {
    fn filter_id(&self) -> &'static str {
        "rate_limiter"
    }

    fn apply(&self, _req: &mut RequestContext) -> Result<(), FilterError> {
        Ok(())
    }
}

pub struct GatewayPipeline {
    // Dynamic dispatch using trait objects
    mandatory_filters: HashMap<&'static str, Box<dyn RequestFilter>>,
    optional_filters: HashMap<&'static str, Box<dyn RequestFilter>>,
}

impl GatewayPipeline {
    pub fn new() -> Self {
        Self {
            mandatory_filters: HashMap::new(),
            optional_filters: HashMap::new(),
        }
    }

    pub fn register_mandatory<F: RequestFilter + 'static>(&mut self, filter: F) {
        self.mandatory_filters.insert(filter.filter_id(), Box::new(filter));
    }

    pub fn register_optional<F: RequestFilter + 'static>(&mut self, filter: F) {
        self.optional_filters.insert(filter.filter_id(), Box::new(filter));
    }

    pub fn process_request(&self, req: &mut RequestContext) -> Result<(), FilterError> {
        // 1. Mandatory filter retrieval:
        // Must use .expect() because system boot contract guarantees 'auth_guard' was registered.
        // Failing to register mandatory core filters is an unrecoverable bootstrap configuration defect.
        let auth_filter = self
            .mandatory_filters
            .get("auth_guard")
            .expect("FATAL_BOOTSTRAP_ERROR: Mandatory core plugin 'auth_guard' was not registered in GatewayPipeline!");

        auth_filter.apply(req)?;

        // 2. Optional filter retrieval:
        // Use non-panicking option handling since optional filters may or may not be installed.
        if let Some(rate_limiter) = self.optional_filters.get("rate_limiter") {
            rate_limiter.apply(req)?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pipeline_successful_authentication() {
        let mut pipeline = GatewayPipeline::new();
        pipeline.register_mandatory(AuthGuardFilter);
        pipeline.register_optional(RateLimitFilter { max_requests: 100 });

        let mut headers = HashMap::new();
        headers.insert("Authorization".to_string(), "Bearer valid_jwt_token".to_string());

        let mut req = RequestContext {
            uri: "/api/v1/resource".to_string(),
            headers,
            authenticated_user: None,
        };

        let result = pipeline.process_request(&mut req);

        assert!(result.is_ok());
        assert_eq!(req.authenticated_user, Some("user_123".to_string()));
        assert_ne!(req.authenticated_user, None);
    }

    #[test]
    fn test_pipeline_unauthorized_failure() {
        let mut pipeline = GatewayPipeline::new();
        pipeline.register_mandatory(AuthGuardFilter);

        let mut req = RequestContext {
            uri: "/api/v1/resource".to_string(),
            headers: HashMap::new(),
            authenticated_user: None,
        };

        let result = pipeline.process_request(&mut req);

        assert!(result.is_err());
        assert!(matches!(result, Err(FilterError::Unauthorized(_))));
        assert_eq!(req.authenticated_user, None);
    }

    #[test]
    #[should_panic(expected = "FATAL_BOOTSTRAP_ERROR: Mandatory core plugin 'auth_guard' was not registered in GatewayPipeline!")]
    fn test_pipeline_missing_mandatory_filter_panics() {
        let pipeline = GatewayPipeline::new(); // empty pipeline
        let mut req = RequestContext {
            uri: "/api/v1/resource".to_string(),
            headers: HashMap::new(),
            authenticated_user: None,
        };

        let _ = pipeline.process_request(&mut req);
    }
}

Technical Explanation

  1. Dynamic Dispatch & Vtables: The pipeline uses Box<dyn RequestFilter>, which creates a fat pointer containing a pointer to the filter instance data and a pointer to the Virtual Method Table (vtable). When filter.apply(req) is invoked, Rust resolves the concrete function address dynamically at runtime via the vtable.
  2. Enforcing Bootstrapping Invariants with .expect(): Mandatory middleware components must be present for security compliance. Using .expect() when retrieving "auth_guard" ensures that misconfigured server deployments fail instantly at startup with clear actionable error messages rather than silently bypassing authentication or failing late with obscure NullPointer equivalents.
  3. Thread-Safety Traits (Send + Sync): Constraining RequestFilter: Send + Sync guarantees that GatewayPipeline can be safely shared across worker threads (Arc<GatewayPipeline>) without data races.
  4. Testing Invariant Violations: Using #[should_panic(expected = "...")] in unit tests verifies that missing mandatory dependencies panic as intended with the exact anticipated diagnostic message.

  • panic! — The macro that is secretly executed when unwrap or expect encounters an error.
  • ? Operator — The safe, preferred alternative to unwrap.
  • Option<T> — Related concept: Option<T>.
  • Result<T, E> — Related concept: Result<T, E>.

7. Key Takeaways

  • .unwrap() instantly extracts the success value from a Result or Option.
  • If it encounters an Err or None, it instantly Panics and crashes the entire program.
  • .expect("msg") does the exact same thing, but allows you to attach a custom error message to the crash log.
  • You should always prefer .expect() over .unwrap() so your future self knows why you thought the sledgehammer is safe to use.
  • Only use these methods if you can mathematically guarantee the operation will never fail (e.g. hardcoded strings), or if you are writing quick, throwaway prototype code.
Built with LogoFlowershow