Best practice, return result or custom enum?

I have some function read() which can fail in multiple ways or return one of several variations.

I am considering if I should use a Result with two enums as the payloads (right word?) so I get all the sugar with Result. Or, should I create my own single enum with all error/success states and avoid having to "double match" the function's return value.

I have seen both patterns in the wild, and glean that the standard library really always uses Result.

Is there a best practice/recommendation/idiom here?

An Example:

enum StreamResult {
    // All of these would have data, but for simplicity . . .
    Message,
    Batch,
    SocketError,
    StreamError,
}

fn read() -> StreamResult {
    // Do stuff and return a result
}

fn main() {
    let result = read();
    match result {
        // Now, I have to match on all four variants
    }
}

This has a single match. It's nice and clean. But, the Error and Success states are mixed. I cannot use unwrap() or the ? or any other fun things with Result

Or:

enum StreamSuccess {
    Message,
    Batch,
}

enum StreamError {
    SocketError,
    StreamError,
}

fn read() -> Result<StreamSuccess, StreamError> {
    // Do stuff and return a result
}

fn main() {
    let result = read();
    match result {
        Ok(success) => {
            match success {
                // Now, I match both variants
            }
        },
        Err(error) => {
            match error {
                // Now, I match both variants
            }
        }
    }
}

This is uglier to call, but allows for unwrap() or ? or many other niceties.

Thank you for your input! I really appreciate learning to write idiomatically.

I really think it comes down to whether you want to use the ? syntax sugar, to my knowledge in order to use ? with your StreamResult, you'd need to implement Try

the RFC book goes over implementing try for exactly the same kind of error type as your StreamResult.
https://rust-lang.github.io/rfcs/3058-try-trait-v2.html#implementing-try-for-a-non-generic-type

Where with the 2nd style, Try is already implement for Result, so you don't need to do anything which requires unstable for it to work.

I'd say unless you have a reason for wanting to e.g. be compatible with an existing #[repr(C)] error, go with the Result.

You don't really need nested match here. The match arm takes pattern with arbitrary depth.

match result {
    Ok(StreamSuccess::Message) => ...
    Ok(StreamSuccess::Batch) => ...
    Err(StreamError::SocketError) => ...
    Err(StreamError::StreamError) => ...
}
9 Likes

The custom enum solution has a big drawback: there is no evident difference between handling a success and an error value.

Standard values are most of the time the best choice, if you can base your code on them you rely on solid and worked in solutions which adapt to the most common cases.

As a counterpoint, I'll suggest that when the domain does not fit the set of available "standard values", then you are better off making your own custom data type. For example, if you have a set of different values to return, which do not break into error and non-error values, stuffing them inside a Result is probably counter-productive.

One common example of (IMO) inappropriately using standard types that leaps to mind is using booleans for when there are two cases, (at the moment,) but they are not clear opposites. You need to go look at the function to understand what the other case is, (and that there is indeed only one other case.) But a well-named enum can make things a lot clearer. This is particularly bad if the boolean gets passed through lots of layers. I find that in those cases a non-trivial amount of the time you end up wanting a third value later on, too.

2 Likes

Thank you all for the great discussion. In this case, I went with splitting my responses into 2 enums and using Result. I can see where other options would be useful, but it seemed idiomatic in this case.

One heuristic that might help: If it doesn't make sense to impl Error for the type you're returning in Err, then it might be time for the custom enum instead.

For example, I've always found the use of Result in binary_search a bit sketchy. Maybe not strictly wrong, but outside of std (where it's easier to add a new enum) I'd definitely rather it use a different return type.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.