Did Rust make the right choice about error handling?

This is not different from C/C++ aside from the fact that Rust complicated using uninitialized memory.

I'm not sure how it is Rust's fail.
Rust falls into the same trap as C++ where you have to think how to optimize your code.
Just because you write Rust or C++ doesn't make your code fastest.

Rust has a lot of fancy stuff like slices and etc to pass pointer to storage so I'd say you should practice Rust a bit more before seeking to blame the tool.

P.s. Do not use examples of Rust code as guidance. If you need optimized code then write it and profile :slight_smile:Just because majority of Rust community doesn't worry about cost of move, doesn't make it language fault

2 Likes

It is certainly more efficient than returning Result because when after the function exit, you have the result exactly when you want it to be (you passed the address where to store it). If the function fails, it will not be there, and you can know about it via return value/errno. If you get Result you have to move the successful result from it.

Just because it's true that jumping into the ocean causes some displacement in the water doesn't make it statistically relevant. The same is true of what is or is not efficient.

2 Likes

Sure. But when it comes to talk about Rust (and C++ too) advantages, everyone mentions zero-cost abstractions/(zero-cost) type-safety etc., which turns out to be a market lie, and in practice we almost always have a trade-off between performance and safety/beauty. And the Rust standard library goes the "beautiful" way :slight_smile:

Everyone knows that zero-cost abstractions are lie, but their cost can be minimized with various techniques employed by compiler.

So don't disregard it so easily, anyway with experience you'll see how to write efficient Rust so I don't advise to become discouraged just yet.
Rust is far more convenient language than C++ for use

3 Likes

Rust goes for making the "correct" way the default and ensuring that workflow is as painless as possible. This "correct" way has to be amenable to static analysis so that the compiler can check your working. This analysis also potentially allows the compiler to perform more aggressive optimizations in some situations.

If there arises a situation where this analysis and optimization fails to produce code that is as performant as you need, then yes you need to change your code. Just like performance optimizations in any language. Only Rust does try to give you the tools to do these optimizations as safely as possible.

1 Like

You're jumping to a big conclusion calling Rust's zero-cost abstractions a lie. It seems you're basing this accusation on only your assumptions of what Rust is doing, which people have even shown you that aren't quite correct.

This sounds like a just an assumption/prejudice again. People here are telling you what they've experienced, what they've measured, how Rust actually works, and you're responding with "no, I feel it's bad".

Rust standard library is carefully written to support zero-cost abstractions. Developers pay attention to how the libstd constructs get optimized, and treat suboptimal code as bugs to fix. The standard library has got many tweaks to make results and iterators optimize to proper zero cost.

While ideal code can't be guaranteed in 100% of cases, it does hold up in practice. And Rust does give you tools to get optimal code in cases where the optimizer can't. In this regard techniques may differ, but possibilities and performance aren't materially different from C and C++.

14 Likes

Maybe my generalizations went too far, but at the beginning I just asked about the general efficiency of design decision to (by default) use sum types for error handling (obviously, borrowed from FP languages with GC).

Rust has layout optimizations for sum types. For example it recognizes when enum variant contains non-nullable pointer, so that null can be used to represent the other case (and even more clever things like union with unicode character will use an invalid unicode character as the other case). If you expect to be returning lots of results, you can use zero-sized error types, for example. This can be so cheap that checked arithmetic in Rust returns Option.

If you expect errors to be rare, you can heap-allocate them to make your result type smaller (if your payload is also heap allocated, you get two pointers which will be returned cheaply). Rust has maybe even too many crates that give you result handing optimized for various scenarios.

You're in control of how large your enums are, and there are even Clippy warnings for bad cases.

The constructor pattern is a great candidate for inlining, and Rust does inlining pretty well, so often there's literally no difference between C-style type tmp; new(&tmp) and let res = new().

Here's an example:

Nested functions return Results of big arrays, multiple times, and the whole thing optimizes down to three instructions, which call a single memset for the combined area on the stack, with no trace of sum types anywhere.

12 Likes

Moderation note: This discussion is going in circles at a rapid pace. In order to keep this discussion constructive, I'd recommend that folks start presenting concrete examples that can be measured. Without concrete examples and measurements, it's too easy to tilt at windmills.

22 Likes

Ok, I have to admit I was wrong. I elaborated the example of @Michael-F-Bryan, and it turned out that rustc is able to optimize at least simple use cases of sum return types:

Maybe I made my pessimistic assumptions because equivalent C++ compiles rather poorly (GCC do even worse):

Sorry, everyone.

28 Likes

Admitting you're wrong takes a certain amount of character and is super rare, so kudos to you @vmgolovin :slight_smile:. :+1:

10 Likes

My name is ZiCog. I'm wrong all the time.

Phew, you are right, that is not easy :slight_smile:

Luckily there are lot's of fine folks here to set us right when need be.

5 Likes

Well said. Could be promoted to QoTW.

6 Likes

Actually on Error Handling I'm with Rust.
When working with Operating System Libraries you far too often find that you are required to provide some complicated Structure to the Routines and actually never know what is the desired State of the Structure.
What later results difficult to check if the Results are correct or erroneous and what is required to get the correct Result.
This even more gets complicated as some Libraries don't provide a correct Initialization Routine

enum Status {
SUCCESS,
ERROR1,
...,
ERRORN
};
typedef struct S {
    uint a1,
    uint a2
    };
void queryData(S *struct_with_data);

Given this API how will you know that you got the correct values in the Fields struct_with_data.a1 and struct_with_data.a2
Does mean struct_with_data.a1 == 0 that queryData() has failed ?
But even struct_with_data.a1 == 0 can be a valid Data State.
Like for Example:
Lunix Signal Handling
the nice siginfo_t Structure.

On the other hand Rust does not let you continue with an invalid struct_with_data after queryData returned and Error Result.

The only Downside of this Error Handling is that it forced to much Result and Option objects with require a more and more Nested Code with on the long run becomes more and more unmaintainable and suffers in legibiilty.

For anyone coming from C I don't think it's controversial to state that error handling is worlds better in Rust. I'd say the same goes for Go, as that uses the same strategy for error detection and management as C.

I happen to think the same thing w.r.t. exception-based languages eg Java, C++ and C#, as exceptions 1. are not exceptional, 2. tend to be extremely slow control structures and so 3. the first 2 factors combine to produce the worst of both worlds.

However, given this very topic, there are still people who hold a different POV and somehow see exceptions as useful, but my educated guess there is that such cases are pretty much always instances of the Blub Paradox.

5 Likes

I think the usual arguments in favor of exceptions are:

  1. They can require less thinking about errors (for better or for worse), since errors aren't part of the function's return type. Rewriting a function to be fallible also takes less effort. (This could be mitigated a bit with syntax sugar, as withoutboats suggested.)
  2. They're useful if a situation is truly exceptional, since there's neither the syntax nor runtime overhead of error handling as long as we're in "the happy path".

Overall, I prefer Rust's approach.

2 Likes

Indeed I think that is the case as well. But handling errors properly when they occur (whether exceptional or not) is fundamental to building properly working software, and so neither can't nor shouldn't be optional regardless of the feelings of people involved. To do otherwise while the tools are not just available but ergonomic to use is lazy (in the negative way, not in the "this motivates me to make better abstractions" way) and irresponsible in my eyes.

As for the performance overhead for using Result-based error management, while it might not quite be zero even on the happy path (the Result types are instantiated after all) it is so close to zero so as to effectively not matter at all (a Result value being created is likely to be just some value being pushed on the stack at runtime, assuming the internal value already existed). I think that's acceptable even for embedded devices, given the rise of Rust in that space.

3 Likes

I would be interested to know what situations people would regard as exceptional and warrant handling with an exception mechanism like we find in C++.

I can think of very few possibilities, like:

  1. Running out of memory. Stack or heap.
  2. A bug in your code.
  3. Hardware error/failure.
  4. Power failure.

For 1) I'd say that in some situations that running out of memory is a bug in your code. You have not taken into account the resources available. Critical if you are building a resource constrained, safety critical, embedded system.

For all of these clearly the best thing to do is kill the program immediately. Get the bug/hardware fixed.

I can't bring myself to count user input errors or missing files or network outages etc, etc as exceptions. They are all input errors and all very common. As such I conclude C++ style exception handling is misguided. As I said above, it encourages sweeping error situations under the carpet and ignoring them. Whereas they are common situations and should be up front in your code. In extreme cases the "happy path" is exceptional !

What do you categorize as an "exception"?

3 Likes

If we keep going with this, it should be explicitly stated that Rust has panics, and panics have a lot of important similarities to exceptions. IMO it's most constructive to think of "traditional exceptions" as somewhere in between Rust panics and returning Results (or more generally, "monadic return types").

So it's probably very hard to think of a good use case for exceptions that isn't already covered by Results or panics, but that alone is not a good argument against exceptions. We have to look at error handling systems a little more holistically than that. Even surface syntax becomes almost critical here: Although I believe Rust has the best error handling story of any language I've tried, I would not believe that if we hadn't developed the try!() and ? syntaxes for Result propagation and instead needed to write an if or match block around every single fallible function call (this is actually one of my biggest complaints about Go).

4 Likes