56 comments

[ 3.7 ms ] story [ 107 ms ] thread
Cool blog. I tried playing around with Rust as well on some toy problems, and had a similar experience: cryptic error messages that only made sense in hindsight.

It's an awesome language, but I wish there was a bit more effort put into into making it 'ergonomic'. Of course, it's an open source effort so nobody is owned anything - but useful error messages are incredibly important, especially when the language pushes so many new concepts.

A fair but common criticism of new languages. Clojure is famous for this problem as well.
Elm, however, goes the exact opposite and tries to make compiler error messages as helpful as possible to beginners, especially from 0.16 onwards: http://elm-lang.org/blog/compilers-as-assistants with a whole repository dedicated to non-compiling programs with less than ideal error messages: https://github.com/elm-lang/error-message-catalog/

TBF might be simpler for Elm as it eschews much in the way of type abstractions.

Yeah, Elm has a far simpler type system at the moment (no ad-hoc polymorphism for example), so it cuts down on the difficulty of experimenting with new ways of displaying errors. Which I think is a good thing to do, especially seeing as one of the goals of Elm is to make static type systems more 'friendly' to a wider audience. But it is definitely going to push the developers of more complex languages to improve their developer experience - we in the Rust world are certainly taking note!
Oh wow, those error messages alone make me want to try Elm. It looks like I could learn the language just from the compiler output.
How could that particular error (using that as an example) be improved?

> error: the trait `core::fmt::Debug` is not implemented for the type `T`

That pretty much is the problem. It seems the bigger pain point is as you say "they make sense in hindsight", but that's because (as the author acknowledges) of expectations driven by other programming languages. C++ wouldn't have raised an error because the type-checking is done after template expansion, whereas Rust is the other way around.

The biggest criticism from me would be the hints underneath advising the author to implement the Debug trait, whereas the solution is to add a trait bound to the code instead.

If anything I'd say Rust's compilation errors are above average in both the accuracy of the error message, and helpful hints. The learning curve around concepts such as lifetimes and moved values and other rare features do take time, however.

(comment deleted)
I would like it to say something like "Either the trait `core::fmt::Debug` is not implemented for the type `T`, or it is implemented but the compiler does not know about it in this context."

Not sure if that is the language I would use, but that is what I want it to say. Suggesting that I might be able to solve the problem by providing more information to the compiler would help me realise how I could solve the problem.

I've had similar problems when developing Rust code, and I wonder whether they would be happy to accept changes to their error messages?

My biggest difficulties trying to understand Rust error messages were either with lifetimes or errors emanating from within macros (it is very hard to see what is failing when you have never seen the code itself.)

Interesting. I'd read that code as "For all T, if we have a Buffer<T>, print out the Vec<T>'s elements" and it makes sense that this doesn't work. It isn't that the compiler doesn't know about it, but that T is not constrained within the form to something printable and consequently you can't print it. It would seem more confusing if it had indeed worked, since that would mean (in a semantic sense) that T is auto-constrained to the types that are actually used as T, so you can't reason about the code having the T on its own but only within the context of the larger form.

Not arguing or anything about what error messages are valid. I just found your perspective about the "compiler not knowing" interesting and felt like sharing this.

Ah, you are right. It's not so much that the compiler doesn't know something, but that the compiler does know something: T could have been an non-printable type. You are being protected against panics that might have resulted from passing vectors of non-printable types in.

I still think the error message could be a lot better, as the word 'implementation' is confusing. What's happened here is that Buffer was being used in a for-loop as if it always contained printable types, when actually there was no assurance that this was the case.

Edit: Highly recommend reading the comments of the two people that responded to me, as they're both more correct and more helpful while I was trying to work things out as I wrote my comments.

In fact noting pretty much what you wrote in the first paragraph would improve it a lot: below the initial error message (above the expansion stuff) it could note

    requires that "i" implements Debug, but

    src/x/mod.rs:15 impl<T> Info for Buffer<T> {
    does not specify that T must be Debug

    hint: you can bound T to Debug:

        impl <T> Info for Buffer<T> where T: Debug {

    this means the code will only work with types which implements Debug, 
    but lets you use Debug operations in your methods
Worse, the rest of the error message gives completely the wrong direction for this error case, it gives an advice for a concrete type T, not for a generic one.
> You are being protected against panics that might have resulted from passing vectors of non-printable types in.

Not quite. Rust's generics are instantiated statically - the compiler generates code specific to the type parameters passed in. The compiler must handle cases where it could not find a conforming function implemented for that the type, otherwise no code can be generated. Without the use of traits to constrain types you could get around this by printing a stack trace of the instantiation, but that causes even worse error messages - ie. the ones you see coming from C++ compilers.

  >  I wonder whether they would be happy to accept changes to their error messages?
We have a special tag just for this in the issue tracker, and care a lot about diagnostics. Please report confusing errors if you see them, and patches are even better.

People seem to be _really_ split about Rust's current errors: I've heard, in the same day, "Rust has the worst error messages" and "Rust has the best error messages". Trying to figure out how to get the former camp into the latter.

Is it possible to have error messages that work for everyone? Is there a fundamental difference between what the former group want out of an error message and what the latter group want?
That's what I am trying to figure out. I have a hunch that it may be correlated with language background.

That's usually why I'm always asking for specifics when people make general complaints, I'm trying to dig into the _why_. "Error messages are bad" isn't actionable; "error messages are bad because" often is. This thread was already getting into specifics before I woke up, though :)

> I've heard, in the same day, "Rust has the worst error messages" and "Rust has the best error messages".

Which probably means there's a steep learning curve to them, but they are really useful and informative after that initial learning period is over. If so, tread carefully lest you ruin the latter by catering to much to the former problem.

I have to agree. I rarely don't know what happened or how to fix it - at least locally. It may completely screw up the types I use elsewhere and cause me to redesign things entirely, but thats more of poor design on my part, I believe.
We put more effort into error messages than almost any language I know of, except Elm and Haskell dialects. A huge fraction of rustc commits are tweaking error messages for ease of use.

Rustc error reporting was deliberately based on that of clang.

In particular, I think it is going to be impossible to have error messages for lifetime errors that are solvable without doing at least some background reading.

I can only offer my point of view as a complete rust novice. Lifetimes are the most intriguing and new aspect of programming in rust - this unfortunately means that when people first start with the language and they run into non-obvious errors, they will need a little handholding.

Google/stackoverflow of course help, but, as an expert, I think you may be a bit biased by your own experience as to how helpful the compiler errors actually are.

> Lifetimes are the most intriguing and new aspect of programming in rust - this unfortunately means that when people first start with the language and they run into non-obvious errors, they will need a little handholding.

I genuinely have no idea how you'd even go around improving lifetime errors, if somebody manages to crack that one I'd probably be willing to set up a recurring donation to whatever they wish.

Well, sure, but it's not that nobody was paying attention to lifetime error messages. It's that it's really hard, maybe impossible, to make lifetime error messages make sense for those who are unfamiliar with lifetimes.
This is pretty much how every language does generics except C++, so it's not actually an issue with Rust.
I don't think it is an issue either. In fact I was pretty happy when I understood the concept.

Unfortunately I haven't got experience with generics in languages other than C++, so thanks for pointing this out.

I've often thought of C++'s templates (when used in the "straightforward" manner, anyway) as more of compile-time duck typing than a real generic type system. Even moving from C++ to Java took a while to get used to.
Moving to Java generics took me a while mostly because of type erasure. In C++, I'm used to being able to use the type in a generic class or function, which kept biting me in Java.
What do you mean by use the type? Query for type metadata at runtime?
(comment deleted)
No, that's not what I meant, but yes, that is a limitation. (Just not one I hit.) The big one is not being able to allocate objects of that type. For example, in C++, I can say:

  template <class T>
  T* allocate() {
    return new T();
  }
That's not possible in Java because of type erasure. When you try something like that, you get an error message like:

  Erasure.java:12: error: unexpected type
              return new T();
                       ^
    required: class
    found:    type parameter T
    where T is a type-variable:
      T extends Object declared in class Foo
Again, this is because of type erasure: Foo<T> is really Foo<Object>, where Java just does a cast for you to T in all of the right places. It does not actually know the type. For me, that lead to all sort of contortions because a class Foo<T> could not allocate objects of type T internally. I needed to create factory methods in other places which Foo would call. In the instance I'm thinking of, those factory methods ended up living in derived classes.

C++ templates have its issues, but it allows parametric polymorphism. In Java, I could not write such code, which was what I was used to. Because of type erasure, I was forced more towards subtype polymorphism, even though I was implementing generic classes.

I'm not quite sure that's due to type erasure. What would happen to this code if T were an interface? Meanwhile even though (AFAIK) GHC uses type erasure, you can do something like that and create values "de novo" if the typeclass supports it e.g. Alternative's `empty` or MonadPlus's `mzero`
Even if you constrain the type to be something you know is a class, it won't work. [1]

The typical workaround in Java is to either pass around .class instances, or to do tricks with anonymous subclasses to create a type token. [2]

[1]: https://ideone.com/th18eh [2]: http://docs.guava-libraries.googlecode.com/git/javadoc/com/g...

> Even if you constrain the type to be something you know is a class, it won't work. [1]

Oh yes, I'm not saying it's possible to do this in Java, AFAIK you're right that it's not, I'm saying I'm not sure it's because of type erasure, at least type erasure on its own.

I think it is. As far as Java is concerned, T is Object in that context. It can't generate code for "allocate a T" because Foo only actually operates on Objects, not Ts. That is, there will be one version of Foo, and that version will generate code to interact with Objects, not Ts. It can't allocate a T because the type of T is erased from Foo. That is type erasure.
Rust's traits seem similar to Haskell typeclasses. I saw the error at once, perhaps for that reason: I could almost hear ghc whine about not being able to deduce Show for arbitrary types.
Rust's traits were indeed inspired by typeclasses, so those coming from Haskell shouldn't have much difficulty with these kinds of errors. The tricky thing is trying to make it easier for those who don't have that experience.
Hardly accurate but coming from the C# world I thought of traits as a hybrid between an interface and extension methods. I think the biggest challenge was that, at first glance, a trait does appear to be very close to an interface, but really it is it's own beast.
The C++ working group has actually tried to provide a solution to this issue - that templates are only type-checked at instantiation time - for almost ten years. The original "Concepts" specification was overly ambitious and was dropped from C++11 - and just this year it was deemed that even the newer "Concepts Lite" proposal is not yet ready for C++17.
22:34 error: the trait `core::fmt::Debug` is not implemented for the type `T` [E0277]

22 println!("trait: {:?}", i);

This error message is fantastic! On line 22, i is of type T which does not implement Debug. The hint is off, but the actual error is super useful.

Turns out if you google "Rust e0277" which is the error code he got, this is the top result: https://users.rust-lang.org/t/generics-or-how-do-i-solve-an-...

Which contains the same solution to his problem.

TL;DR Nothing to see here, just a case of "let me google that for you"

edit: please don't downvote finnyspade into the negatives, that's not helpful

> The hint is off, but the actual error is super useful.

I completely disagree. The actual error is very basic and only useful if you already know what it pertains to, that the hint be way off is actively harmful.

> TL;DR Nothing to see here, just a case of "let me google that for you"

No, it's a case of the compiler not helping and just dumping the raw error in your lap, let's not settle for being GCC when we can do better: http://elm-lang.org/blog/compilers-as-assistants

You shouldn't need to google a bloody traits bound error (and may not even be able to, because you're trying Rust on a bus during your commute and have run out of data) (note that the rustc --explain output doesn't quite help either, it also assumes you already understand trait bounds and also suggests implementing the relevant trait on your type, which is the other way around from the issue, a careful read of the code snippets may hint at the solution, but the explanation doesn't point to it)

> The actual error is very basic and only useful if you already know what it pertains to

The error is really solid at showing a type mismatch. Which is of the form "I wanted X but you gave me Y".

> that the hint be way off is actively harmful That's true and they could probably do a bit better for type mismatches on type variables vs concrete types

> just dumping the raw error What's the difference between a "raw error" and something else. If the hint was accurate would it still be a "raw error" because it's not styled like those elm errors? Any more info (aside from a better hint which I concede should be better) would have to be an actual explanation of static typechecking which seems a bit extreme.

All I'm saying is while this error isn't a paragon of what all good error messages should be, it's no more cryptic than any other similar error. Even the nicely formatted elm errors say essentially the same thing.

It's a reasonably easy error to figure out if you are used to type systems like Rust's, but for those coming from dynamic languages or templated languages like C++ it might take a bit more of a leap to figure it out. It's nowhere near as awful as in C++, but it there is always room to improve!
> The error is really solid at showing a type mismatch. Which is of the form "I wanted X but you gave me Y".

That's not really high praises, "I expected Foo and you gave a Bar" is pretty much level 0 of static type checking which even javac manages, you'd have to work hard to fail to provide even that.

> What's the difference between a "raw error" and something else. If the hint was accurate would it still be a "raw error" because it's not styled like those elm errors?

No, that's the point, if you have extensive hints and decorations it's not just the raw errors anymore.

In fact the Rust compiler/developers (to their credit) try not to provide raw errors, they provides code pointers and hints and macro expansions, the issue is for this error they're all unhelpful to downright incorrect.

> Any more info (aside from a better hint which I concede should be better) would have to be an actual explanation of static typechecking which seems a bit extreme.

That's not true, it could be an explanation of traits and trait bounds, and that does sound helpful to Rust beginners (and not harmful in the least to old hands).

> Even the nicely formatted elm errors say essentially the same thing.

The essentials are the hardly helpful[0] same in every compiler, Elm's error messages are contextual not just in the decoration of the code but in that the text uses more precise terms depending on the error's context, and they provide better explanation and hints.

The compiler doesn't have to whack your fingers with a ruler and yell "WRONG".

[0] they're helpful if you have experience in the cryptic ways of the specific compiler for the specific language, if that's our benchmark G++'s historical error messages are helpful, that doesn't seem to be a majority view

> That's not really high praises, "I expected Foo and you gave a Bar" is pretty much level 0 of static type checking which even javac manages, you'd have to work hard to fail to provide even that.

If you know the language well, there really isn't anything more to say - your code wants the `Debug` bound, but it is not present. Either change your code or add the bound.

I am going to improve the error reporting to use a better error message in this case.

> If you know the language well

Right, that's the problematic assumption. The compiler message aren't going to be problematic if the compiler already trained you.

While I don't disagree with your point, I don't agree with your reason. It's not that the compiler has trained you, it's understanding that you must bound generics by a trait to be able to call methods on the resulting value. That's a language-level understanding rather than a reading of tea leaves.

This is also one reason why --explain was added; hopefully the extended diagnostics can help with unfamiliar errors. More experienced users need --explain less.

> it's understanding that you must bound generics by a trait to be able to call methods on the resulting value. That's a language-level understanding rather than a reading of tea leaves.

Sure but here's the thing: the compiler could know report that, as you note that's language-level understanding, who's to better know the language than the compiler?

> It's not that the compiler has trained you

Maybe saying that you[0] trained yourself (in this case to pattern-match that a type mismatch between a generic T and a trait is pretty certainly a missing bound on T) would be a better way to put it?

The point is, the compiler could[1] be much more precise in its diagnosis, and much more assisting and "mentoring" in its output, but a few people argue that it's unnecessary, because they've built not just a mental model of rust but a library of pattern-matching "low-level" compiler errors into higher-level language errors and can do it in their sleep at this point.

[0] generic, you, me, pcwalton, dons, oleg, basically anyone working with a compiler

[1] I am very much not saying it's easy

I do certainly agree that we can do a lot more. There's always so many things to do...
Time to get this cloning thing up and running, might be easier to get more of you than to get you alone to fix it all.
I don't see how Elm error messages are significantly better than those of rustc in this case. Elm says "expected Foo, found Bar" (with more flowery prose). Rust says "expected Foo, found Bar".

The examples on that page are all extremely simple type errors in the first place, whereas this error message relates to a much more advanced feature (typeclasses/traits). I'd like to see what the ideal trait mismatch error would look like.

I do think that this error could be improved by suggesting #[derive(Debug)], don't get me wrong. But I don't know of any compiler for any language that goes out of its way to explain how to implement an interface on every interface-not-implemented error.

> I do think that this error could be improved by suggesting #[derive(Debug)], don't get me wrong.

That's what it's already doing:

> src/x/mod.rs:22:33: 22:34 note: `T` cannot be formatted using `:?`; if it is defined in your crate, add `#[derive(Debug)]` or manually implement it

and it's wrong, the error is a missing trait bound not a missing interface implementation

Oh, I see. That's just a straight-up compiler bug as far as I'm concerned. Doesn't have much to do with error message philosophy, really. Let's fix it :)
That's a little bit too harsh. The solution is a no-brainer if you come from C#/Javaland, but if you don't... well, we get a nice illustration to the saying that "there are no stupid questions".
right when i was about to jump into the elixir / phoenix bandwagon, thinking its type hints would be enough for me ( who like types to be as strong as possible)...

damn... when is the erlang pure actor model ( as welle as OTP) going to be implemented in another language ??

I am waiting for that too. Especially Rust + OTP would be a big thing for me.