Adding a third error-handling type for when no added context is needed

The Problem

Throughout my ~1 year of programming in Rust, I have semi-frequently found myself in a situation where I need to know whether a function that would otherwise return nothing/() succeeded or failed, but not why it succeeded or failed. I could do this one of four obvious ways without using any external crates.

  1. I could use a bool, using true to indicate success and false to indicate failure.
  2. I could use Option<()>, using Some(()) to indicate success and None to indicate failure.
  3. I could use Result<(), ()>, using Ok(()) to indicate success and Err(()) to indicate failure.
  4. I could use a custom enum, using a Success and Failure variant. I do not think any of these solutions are sufficient. I have listed my reasoning for disliking each of these methods below (in their respective order).
  5. bools do not carry the requisite semantic meaning, making my code much less-readable.
  6. Option<()> is somewhat of an antipattern and communicates the wrong semantic meaning. "There could be some nothing, or nothing here" is basically nonsensical.
  7. Result<(), ()> is also an antipattern in my opinion, as Results are explicitly meant to be used for enumerated error types, except in the case of Infallible. In addition to this, it prevents you from using ? for Option<T> returns, or for a Result type with different generic parameters (which is pretty much all of them).
  8. Custom enums lack standardization if I were to expose this type in a public interface, and they also prevent me from using the ? operator entirely.

The Solution

I think that std should implement the following type (names are tentative):

enum Status {
    Success,
    Failure,
}

The reason why I think that this should be in std, and not just some 3rd-party crate, is that it would allow Rustc to support the ? operator as a lossy conversion for both Option<T> and Result<T, E> in a function which returns Status. Without implementation into the language, the only way I could see this being possible is by using procedural macros to tag functions, which tends to have a major negative impact on compile times, and can often cause problems with Cargo check/RA/Cargo clippy.

Why?

  1. Enumerated error types are great, but they are not always necessary.
  2. Some errors cannot accurately be classified, or at least not usefully. This is especially the case when it comes to interacting with external systems and certain FFI systems.
  3. The current non-enumerated error type does not have the correct semantic meaning to be used for this problem.
  4. Some crates end up using something like anyhow because their error types are not easily-enumerable, and in some of these cases it's because the crate is basic and the developer does not feel that their users will have any need for robust error types. Though it is generally discouraged for crates to lack error handling in this way, it seems like something of a "harm reduction" (sorry for the loaded term here) method to prevent careless crate developers from forcing their consumers to use anyhow.
  5. Not every crate is a library. Binary developers should have better flexibility as to how they handle errors, as it is not their responsibility to provide a good interface to other programmers - only to themselves and their teams.
  6. This would help to break the tendency of Option<T> and Result<T, E> types to be close to mutually-exclusive in a project due to poor ergonomics. Though it's hard to communicate this feeling exactly, I suspect that most readers will understand what I mean.
  7. Though I would say this tentatively at best, as someone who has never contributed to std, this might be a way to provide an obvious error return for functions such as Vec::insert() for which reasonable and common error cases will cause a panic. This is based on my understanding that one of the main reasons that a panic is used in place of a Result is due to ergonomic concerns. I may be completely incorrect in this regard, so feel free to correct me.

I am happy to hear your feedback on this, please let me know if you have any questions.

This is actually quite similar to ExitCode, although that has whole-process connotations that you maybe don't care about. Perhaps you could provide a couple motivating examples, and expand upon why this particularly needs to be in std, and can't be locally defined or at least in its own crate.

I agree, unfortunately (as you mentioned) it carries too much baggage related to processes, and additionally would still lack support for the ? operator in conversions.

Where is this written? std has non-enum, non-Infallible error types, for example.

What exactly are you proposing? There's no more error data in your proposed type than in a Result<_, ()>.

1 Like

I did not mean by "enumerated" that it must be an enum, only that it must have some way of representing variants, which is often done with a struct that stores a variant enum in a kind field.

I'm not sure exactly what you mean by this. My point was that the Status enum would be removing any of the data carried by the Result.

Perhaps you could provide a couple motivating examples, and expand upon why this particularly needs to be in std, and can't be locally defined or at least in its own crate.

Show the Try and related implementations and demonstrate how it improves the stated scenarios.

I would probably use a unit struct (precedence: std::fmt::Error ) and Result<(), MyOperationFailure>.

9 Likes

I'd also use a unit struct with Result<(), MyOperationFailure>.

Other precedents: std::sync::mpsc::RecvError, std::​thread::AccessError and std::alloc::AllocError.

The custom unit struct has the advantage over Result<(), ()> that you can implement From<MyOperationFailure> for other error types to make using ? easier.

Also, for any Result<_, _> you can write fn_returning_result().ok()? to use it for Option<T> returns.

If it makes your code easier to read for you, you can then emulate your proposed enum Status with

type Status = Result<(), MyOperationFailure>;
const SUCCESS: Status = Ok(());
const FAILURE: Status = Err(MyOperationFailure {});

without any additions to the standard library and without restricting the use of ? for consumers.

5 Likes

If the Try trait is ever stable, you will be able to make your own result-like types.

BTW, ControlFlow is the third Try type already.

4 Likes

I sometimes wish () implemented Try because then we wouldn't need both for_each and try_for_each a single function would suffice because

foo.iter().try_for_each(|a| {
    a.baz(); // implicit `()`
}

and

foo.iter().for_each(|a| {
    a.baz();
}

would be identical.

second is being able to use Try on an Option in a function which returns ()

I think it's a bit too late for this, for_each has been stable for a long time and is not going away. There's also an argument to be made that some iterators may be able to implement for_each in a faster way due to not having to support early exit.

For this () doesn't need to implement Try, it only needs FromResidual<Option<Infallible>>.

3 Likes

Point 1 I was thinking try_* could go away, but I agree it's probably more work than it's worth. For more efficient implementation you could use specialization if it ever stabilizes.

Point 2 would be great.

Currently being discussed:

1 Like

Frame challenge: I think you're mistaken when you say you don't need to know why something failed. Please provide concrete examples of cases where, in your opinion, the root cause of the failure is unimportant, so we can assess them from our own perspectives.

(Full disclosure: my perspective involves going on 30 years of trying to troubleshoot programs written by other people who didn't make their error reporting as detailed as it could have been.)

5 Likes

I thought implementing Try on () was a cool idea, until someone pointed out that it makes println!()? compile, even tough it doesn't do anything. That'd be super misleading. It will need ok-wrapping magic instead.

2 Likes

There could be a rustc lint against using ? on the concrete () type.

Yes, that was also brought up as a possibility in the ACP: https://github.com/rust-lang/libs-team/issues/187#issuecomment-1745411419

Result<(), ()> being an antipattern is news to me. Well, it is indeed an antipattern, but the actual antipattern is not giving detailed errors. So your proposed enum is as much of an antipattern as Result<(), ()>. Type alias it to Status and you're good to go in the cases where you truly don't care (which is very rare!).

I see no reason to add a new type for this.

6 Likes

But the Try impl for () impl doesn't exist (and shouldn't exist IMO)