Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I think the complaint is more that Rust has seemingly tried very hard to make error handling "simple". But in the process it has managed to invent a whole series of new idioms and special syntax that is alien to pretty much everyone. There's a thread in /r/rust about this same article where you can look and see people suggesting all sorts of ways to write this that are split into clear sedimentary layers depending on when the writer learned the language.

At this point the cognitive load required to read and understand Rust implementations of "typical" practical problems is rather higher than it is for C++. And it seems to be getting steadily worse from my perspective on the outside.



> There's a thread in /r/rust about this same article where you can look and see people suggesting all sorts of ways to write this that are split into clear sedimentary layers depending on when the writer learned the language.

As someone that participated in that conversation, I think that's a pretty inaccurate characterization of it. It's not about when the writer learned the language, but rather, what problem you're trying to solve. If you'll allow me to summarize very briefly (perhaps at the expense of 100% accurary):

    * Use unwrap/expect when you don't care.
    * Use `try!`/`?` with Box<Error> in simple CLI applications.
    * Use `try!`/`?` with a custom error type and From impls in libraries.
    * Use combinators (e.g., map_err) when you need more explicit control.
You might imagine that you could use any number of these strategies depending on what you're trying to do, which might range from "a short script for personal use" to "production grade reliability."

All of this stuff was available at Rust 1.0. (Except for `?`, which is today an alias to `try!`.) It all falls out of the same fundamental building blocks: an `Error` trait with appropriate `From` impls.

The one exception to this is that, recently, there has been a surge in use of crates like error-chain to cut down on the code you need to write for defining custom error types and their corresponding `From` impls. But it's still all built on the same fundamental building blocks.


"new idioms and special syntax that is alien to pretty much everyone"

->

"an `Error` trait with appropriate `From` impls."

Note, that it may be entirely necessary for us to invent new idioms to make progress in the art of programming.


I will say that as someone who was not a real expert at anything when Rust came out, but familiar with a lot, I find that learning c++ these days has the same problems, and so does learning a functional language.

They're all unique in what they do. I'd still say a simple procedural language like C is easiest to read, but 'modern c++' has more idioms and quirks than Rust, in my experience.

And Rust has the advantage that the ecosystem is very well tied together and it has a great community and documentation.


It might be worth pointing out that this Rust:

    #[derive(Debug)]
    enum ConfigError {
        Io(io::Error),
        Parse(ParseIntError),
    }
    
    impl From<io::Error> for ConfigError {
        fn from(err: io::Error) -> ConfigError {
            ConfigError::Io(err)
        }
    }
    
    impl From<ParseIntError> for ConfigError {
        fn from(err: ParseIntError) -> ConfigError {
            ConfigError::Parse(err)
        }
    }
    
    fn read_config() -> Result<i32, ConfigError> {
        Result::Ok(parse_int(read_config_file()?)?)
    }
    
    // given the following
    fn parse_int(str: String) -> Result<i32, ParseIntError> { ... }
    fn read_config_file() -> Result<String, io::Error> { ... }
Is, in a sense, the equivalent of this Java:

    int readConfig() throws IOException, ParseException {
      return parseInt(readConfigFile());
    }
    
    // given the following
    int parseInt(String str) throws ParseException { ... }
    String readConfigFile() throws IOException { ... }
The reason i say that is that this:

    throws IOException, ParseException
Is essentially a sum type. It says that if this method results in a failure value, it can fail with one of two types of failure values. It might not look like a new type, because in Java, types are almost always nominal, and this is structural, but that's what it is. I think that throw and catch clauses are the only place that Java will let you define an ad-hoc sum type. You have to use polymorphism everywhere else you want a variety of types.

Whereas in Rust, there are no structural sum types, and so the only way to make something resembling a sum type is:

    enum ConfigError {
        Io(io::Error),
        Parse(ParseIntError),
    }
Which means you also have to write the machinery to convert between the types.

I wonder if it would help to have a compiler- or library-defined From impl for all newtype enum variants (or all newtype structs more generally), that makes the variant from its argument. Or maybe it could be derived. It would wipe out a lot of this boilerplate.


I would love a `#[derive(From)]` for newtype structs and enum variants. Would bring Rust error handling back below Java in boilerplate levels. :)


There are some crates that add a derive for errors, but most people tend to use error-chain or quick-error. Both give you the boilerplate pretty much for free, with various other advantages. E.g., using error-chain, you can just write the above code as

    error_chain! {
        foreign_links {
            Io(io::Error);
            Parse(ParseIntError);
        }
    }
and it'll even generate an aliased `Result` type as well as fancy chaining support.


Right- error_chain is just a bit too much magic for me compared to what #[derive(From)] would do.



Very nice. Now we just need enum variants to be first-class types, and some syntax for deriving on them.


In case someone cares to understand what this means:

- "An Error trait" means that when you define a new type that will store error information, you have to define how it implements the Error interface.

- "appropriate From impls"... you are trying to wrap a number of error types in your own special error type, you need to tell the compiler how to convert another specific type into your type new type. There is an interface (trait) in the standard library for this purpose called "From". This is done as an alternative to inheritance in an error system. The trait signature looks like this:

    trait From<T> {
        fn from(T) -> Self;
    }


Sorry, I'm trying very hard to learn Rust, and I'm willing to accept a lot of its restrictions, but this didn't clear anything up.

Why does every library implement its own Error type? It feels like reinventing the wheel, and it takes a lot of boilerplate code.

If you need to distinguish different kinds of errors, why not, for example, have a lot of useful pre-defined error types like Python does?


You can definitely reuse other library's error types. I often do that while prototyping.

The benefit of lib-specific error types, though, is that they can be far more specific.


As an example of the kind of specificity you might want, take a look at the error type for parsing a regular expression: https://docs.rs/regex-syntax/0.4.0/regex_syntax/enum.ErrorKi...

Now, in most cases, callers don't care at all about which specific error happened. They just want to print the error and be done with it. And that works just fine, because of the error handling machinery. But if you do want to drill down, then the option is there for you.


You can use pre defined error types. Have you seen https://doc.rust-lang.org/std/io/enum.ErrorKind.html ?


That seems to just be kinds of IO errors, though.


> The one exception to this is that, recently, there has been a surge in use of crates like error-chain to cut down on the code you need to write for defining custom error types and their corresponding `From` impls. But it's still all built on the same fundamental building blocks.

One takeaway I got from playing around with Rust (and using GitHub code search to work through my issues) was that coding style will probably differ considerably from project to project, which is very C/C++ like and maybe a good thing for the language, but I found a little disappointing.


We are actively working on rustfmt, which should add some consistency overall. Some people won't use it, of course, but many have said they will.


The kind of 'coding style' that is varying here is probably more in the domain of Clippy than rustfmt. But there are people working on that too!


Yes, that's what I meant. Many ways to do things and those ways can differ significantly.


I think the complaint is more that Rust has seemingly tried very hard to make error handling "simple". But in the process it has managed to invent a whole series of new idioms and special syntax that is alien to pretty much everyone. ... ways to write this that are split into clear sedimentary layers depending on when the writer learned the language.

That's been my criticism of Rust error handling. Rust's error handling system is very clever. It's logically sound. It manages to make functional programming and error handling play well together. But it's not user-friendly. For a while, it took far too much code to handle errors. So gimmicks were developed to make the necessary gyrations less verbose. These hide what's going on underneath. Thus the generations of error handling approaches.

Rust tried to avoid the complexity of exception handling, but ended up with something that's more complicated. Python programmers, who have a good exception system, notice this. In Python, you write the main case, and then you write an exception handler to deal with the error case. This works well in practice. Python has an exception class hierarchy. If you catch EnvironmentError, you get almost everything that can go wrong due to a cause external to the program. If you catch IOError, you get all I/O-related errors, including all the things that can go wrong in HTTP land.

With exceptions, if you're using some code that doesn't handle an error well, you can catch the problem at an outer level, get good information about the error, and recover. With error-value returns, after you've come through a few levels of function returns, you're usually down to "something went wrong". (Having written a web crawler, I've found this useful. A huge number of things can go wrong in HTTP, HTML parsing, SSL certificate handling, and the other manipulations needed to read a possibly-hostile web page. A crawler needs to catch all those and deal with them, deciding "try again now", "try again later", "log error and give up", or "try alternative access approach". This makes one appreciate a good exception mechanism.)

Exceptions have a bad reputation because Java and C++ implement them in ways that are inferior to Python's approach. There's no exception hierarchy. Knowing what exception something can raise is very important. Often, you don't.

Rust (and Go) are slowly backing into exception handling, as the panic/recover mechanisms acquire layers of gimmicks to make them more useful. Rust already has unwinding (destructors get run as a panic event moves outward), which is the hard part of exception handling. Thus, exceptions are more a religious issue than a technical issue.


There's no exception hierarchy. Knowing what exception something can raise is very important. Often, you don't.

Java has an exception hierarchy and exceptions as part of method signatures. If anything, Python's exception hierarchy started getting sorted out relatively recently.


Somewhat off-topic, but I'm curious to learn about the relatively recent changes in Python's exception hierarchy, could you point me to a link?


As someone who's been doing C++ for a close to few decades I's challenge you on that. In C++ you need to understand:

Exceptions(and the runtime/memory costs they incur by pulling in RTTI)

ERRNO(on relevant *nix platforms)

Lifetimes tied to objects when things fail(this is a big one)

Plus any library-specific hackery(I've seen raw strings as errors before)

In contrast I've been writing Rust for ~1.5 years now and each library I've used is consistent and follows common patterns. Much like a lot of people don't grok functional until they understand the common patterns, so it is with Rust too.


Exceptions don't need RTTI and have only runtime cost when thrown.


Exceptions actually can need RTTI, though not necessarily all of the RTTI that things like <typeinfo> provide. For some details, see the -fno-rtti flag for GCC, for which the documentation says[1]:

Disable generation of information about every class with virtual functions for use by the C++ runtime type identification features (`dynamic_cast' and `typeid'). [...] Note that exception handling uses the same information, but it will generate it as needed.

[1]: https://gcc.gnu.org/onlinedocs/gcc-4.6.1/gcc/C_002b_002b-Dia...


That is a compiler specific implementation, the ANSI C++ standard doesn't require it.


That's great and all but when every modern compiler does implement it that way it's not something you can ignore.

Show me a compiler that's used in production which handles multi-inheritance exceptions without allocating extra data and I'll be happy to eat my words :).


Sadly I cannot pay for all commercial compilers out of my own pocket. :)


The ANSI C++ standard requires exceptions in the first place. The moment we start talking about disabling them, we're not talking about standard C++ anymore.


Which is why people cannot just state "In C++ the thing X happens" without regarding what the standard says, instead of what the installed compiler does.

Language != Implementation.

If the language would be "In GCC the thing X happens when ...", then ok.


For all practical purposes, the most popular implementations are the language. You can only ignore that in an academic context.


Popular where? Not everyone uses GCC.


> Exceptions don't need RTTI and have only runtime cost when thrown.

No, they add extra control flow edges, which inhibit optimizations, affecting runtime performance. This is a lot of the reason why unwinding is optional in Rust.


> At this point the cognitive load required to read and understand Rust implementations of "typical" practical problems is rather higher than it is for C++.

I code in Rust and C++ every day, and am mostly equally experienced in both (maybe more Rust now, but this wasn't always the case). I disagree. Rust has some ergonomics issues that C++ does not, but the reverse is true too. Looking at rust from C++ you'll only see one and not the other, because you're used to the other.

(And as burntsushi said your characterization of the thread is inaccurate, people are suggesting things that are best for different use cases)

Rust hasn't really tried "hard" to make error handling simple. We have what we had during 1.0, and then we have the ? operator, which always existed as try!().


It didn't invent any of these. This is railway-oriented-programming[1], using Either, leftMap and bind/flatMap, a conversion from Either to Try, with forced recover calls and explicit implicit conversions via typeclasses.

Other than the enforcement by the compiler, this is common in scala and f#, two other languages where raising errors is common.

Railway-oriented-programming is nice because it typechecks, and because it reduces cyclomatic complexity by short-circuiting without forcing you to handle the error until the end of the chain. Now, it is unfamiliar to those outside the FP community, but much of the rust language seems to be unfamiliar mixes of FP and low-level optimisation techniques.

With the Either type you don't even need to do that, but then errors on the left becomes convention instead of an enforced habit. The only odd part is the try macro. That should wrap the Val in a right, to preserve type information. I guess I could be convinced that Either = Error | Id [A].

[1]: https://fsharpforfunandprofit.com/rop/


For reference, here is the link to the reddit thread:

https://www.reddit.com/r/rust/comments/69i105/


Maybe I was doing C++ wrong all those years, but I find the cognitive load for Rust about equivalent. The error handling idioms of Rust exist, more or less, in well-composed "--no-exceptions" C++ code. The syntax might be a tad different, but the principles are more or less the same.


That and the bizarre thinking that ".unwrap" is a perfectly ok thing to write in some cases (don't worry it will never make it to production!).

No, ".unwrap" turns input errors into bugs, and there is no production code where this is more desirable than an exception.


I don't get it. .unwrap() turns an undesirable situation into printing an error message and exiting with a nonzero status. There are plenty of situations where that is exactly what you want, and not a bug at all.

For example, in my production Rust code, i deal with all errors in reading and parsing config files with .unwrap() or .expect(). If a program cannot read its config file at startup, it cannot correctly do its job, and so the only correct thing for it to do is to abort.

Have i misunderstood what you were trying to say?


Exceptions are well suited for parsing errors because they embed an error message.


Prints to where? stderr? Fine for a simple CLI tool, but people might want to do something more complicated when an "unrecoverable" exception occurs.

Take for instance a web framework like Django that responds with a 500 error page with a stack trace when an exception occurs.

There are interesting problems to be solved in the space of error handling, e.g. error handling in asynchronous code. But Rust is not even matching the state of the art achieved by Lisp and Python decades ago.


...then don't use unwrap(). Instead check the error, do whatever handling you want and exit gracefully.

unwrap()'s behaviour is essentially the same thing as an uncaught exception in Python, only you can actually check for where they occur in the code with a simple grep rather than hoping your test suite caught every possible failure case.


One of the hardest things about teaching error handling in Rust is that there is a lot of nuance. It's fun to say "unwrap is evil, don't use it," and more than that, it's not exactly wrong either. The nuanced version is that you shouldn't use unwrap for error handling, but:

1. If you're prototyping or writing a quick throwaway program, then unwrap will neither pick your pocket nor break your leg.

2. If you have an invariant that you either can't (or won't) move into the type system, then unwrap/expect/panic'ing can be appropriate precisely because if that unwrap gets tripped, then that will indicate a bug in your program that should be fixed.

The more succinct advice that I like to use is this: "if end users of your Rust application see a panic, then you have a bug." But this is slightly harder advice to follow because it's a statement about the user experience.


> 2. If you have an invariant that you either can't (or won't) move into the type system, then unwrap/expect/panic'ing can be appropriate precisely because if that unwrap gets tripped, then that will indicate a bug in your program that should be fixed.

Absolutely but using the same mechanism (unwrap/panic) for both types of errors - recoverable and recoverable - can creates confusion. panic'ing for wrong user input for example as can be seen in example code.


panics aren't for recoverable errors, in any circumstances. It is technically possible to "halt" the unwinding from a panic, but 1) that's intended for C interop, 2) the feature has been deliberately designed to make your life hell if you try to use it to emulate recoverable exceptions, and 3) Rust makes no guarantees that panics will unwind at all, it is entirely legal for a user to configure panics so that they all abort instead.


> panics aren't for recoverable errors, in any circumstances.

And I understand that. Hence why unwrap everywhere is harmful.


Er, it was a response to this line of yours:

> Absolutely but using the same mechanism (unwrap/panic) for both types of errors - recoverable and recoverable

Nobody is using panics with the intent to recover, in the classic sense of "recoverable error".


See above where one user is using .unwrap for parsing errors (input errors are recoverable).


I think this discussion is muddying the meanings of "recoverable" between "recoverable as a class" and "recoverable in this instance."

Parsing errors are recoverable as a class—you haven't irretrievably corrupted your process memory when you encounter one. Therefore, the parser itself should not panic(); it should just return an option type.

Parsing errors may very well be unrecoverable in a particular instance. The code calling the parser has every right to decide to unwrap() the option type such that a panic() will happen if the parse failed. The code calling the parser is likely business-logic code of an application, and is privy to knowledge like "if there is no configuration supplied here, then later code that tries to consume the configuration will have to crash" and so can decide to early-exit with a user-comprehensible error ("you don't have a config file!") rather than letting the later code crash with some weird error about a config value being Nothing.


Yes the problem is with the grey area between recoverable and unrecoverable.

.unwrap is only suitable for the unrecoverable case, while exceptions are suitable for both cases. Hence why .unwrap in examples is harmful imho.


Not in all cases. Most daemons will die if you give them an invalid configuration file (with a description of the syntax error).




Consider applying for YC's Winter 2027 batch! Applications are open till November 2.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: