1. 22
  1. 3

    For any Rustaceans, I built woah for exactly the need this article lays out. It provides a type woah::Result<T, L, F>, where L is a “local error” you can handle, and F is a fatal error you can’t. woah’s Result type propagates fatal errors up with the ? operator, giving you a std::result::Result<T, L> to handle local errors.

    It’s pre-1.0 until the Try trait is stabilized, but can be used on stable if you’re willing to forgo the ? operator and do the propagation manually.

    1. 1

      Managing multiple different levels of errors is still difficult for me in Rust. I’ll take a look at the crate but I’m wondering if you can explain what the advantage is of using that over Result<T, LayerError> where

      enum LayerError {
          DomainErr1,
          DomainErr2,
          ...,
          IOErr(std::io::Error)
      }
      
      impl From<std::io::Error> for LayerError { ... }
      
      1. 1

        The difference is in having separation between the “local errors” and “fatal errors” and how that interacts with the ? operator.

        woah::Result is equivalent to Result<Result<T, L>, F> in terms of how it works with the ? operator. Take the following example, copied from the project’s README.md:

        use woah::prelude::*;
        use rand::prelude::*;
        
        fn main() {
            match get_data() {
                Success(data) => println!("{}", data),
                LocalErr(e) => eprintln!("error: {:?}", e),
                FatalErr(e) => eprintln!("error: {:?}", e),
            }
        }
        
        /// Get data from an HTTP API.
        fn get_data() -> Result<String, LocalError, FatalError> {
            match do_http_request()? {
                Ok(data) => Success(data),
                Err(e) => {
                    eprintln!("error: {:?}... retrying", e);
                    get_data()
                }
            }
        }
        
        /// Make an HTTP request.
        ///
        /// This is simulated with randomly returning either a time out
        /// or a request failure.
        fn do_http_request() -> Result<String, LocalError, FatalError> {
            if random() {
                LocalErr(LocalError::RequestTimedOut)
            } else {
                FatalErr(FatalError::RequestFailed)
            }
        }
        
        /// Errors which can be handled.
        #[derive(Debug)]
        enum LocalError {
            RequestTimedOut,
        }
        
        /// Errors which can't be handled.
        #[derive(Debug)]
        enum FatalError {
            RequestFailed,
        }
        

        The key line is the match do_http_request()?. The ? operator will pass any FatalError up to the caller of get_data, and return a std::result::Result<String, LocalError> which is what get_data is matching on. In the error case on that match, we have a RequestTimedOut, so we can retry.

    2. 3

      Some call this offensive programming. Does it have a better name? See also:

      https://en.wikipedia.org/wiki/Offensive_programming

      1. 10

        Erlang is a language that is particularly amenable to this style of programming because Erlang processes (actors) are self-contained components. There is no shared mutable state in Erlang and so a process defines a clear failure domain. If an Erlang process crashes, then there are hooks such as monitors that allow you to do something in response to the failure.

        This comes from roughly the same observation that led to exceptions in other languages: some classes of fault can’t be handled locally. The file-not-found or division-by-zero errors in the article are great examples of this: the code that triggers them doesn’t know how to fix the conditions that led to the error and so dies and punts the error up to something that does. The main difference with Erlang is that the error handling is not coupled to lexical scope.

        The other key idea in Erlang is that no component should ever be assumed to be reliable. Even if the code is perfect, it runs on a computer and these are physical machines and are subject to electrical faults and so on. If you want to build reliable systems (and, remember, Erlang came from telecoms systems where five nines of uptime meant that you were missing your SLA by a large margin) then you need to assume that any component can fail and have a recovery mechanism. You build these top-down in your system, starting from the top-level architecture. If you build error recovery here then you discover that very large parts of your program can completely fail without impacting the entire system. Below this point, you can completely punt on any kind of local unexpected error handling because you’ve already built the error recovery systems.

        I think the second point is the key difference between the Erlang model and exceptions. Exceptions encourage bottom-up reasoning about failure. For each component, you enumerate the set of ways in which it can fail and then build mechanisms for handling them as you compose systems. With Erlang, you assemble failure domains at the top level of your design and worry about how to ensure that they are independent failure domains, not about the myriad ways in which a component might fail. This is more robust because it requries you to enumerate the things that can fail, not the reasons that it may fail. The former is an intrinsic part of your system architecture, the latter is an emergent property.

        1. 2

          In the book The pragmatic programmer this strategy is called “Crash early”.