Application error messages that are friendly and detailed

Hello. I think that generating good error messages in user-facing programs is very important. I am frustrated when I see messages like "Connection failed." or "Failed to save file.". Just to be clear, I am talking about the error messages generated at runtime by a program written in Rust; I am not talking about errors from the Rust compiler itself.

I have written several GUI applications in C# and C++ for accessing USB devices that generate error messages that look like this:

Failed to connect to the device. Unable to get the firmware version from the device. USB control transfer failed. Error code 0x1f.

Each sentence in the error message is generated by a different level of the call stack: the first sentence comes from a high-level part of the GUI that is just trying to connect to a USB device. The second sentence would probably come from a cross-platform library for accessing the device. The third sentence would come from the USB abstraction layer (which allows us to write cross-platform code for accessing USB devices). The error code in the fourth sentence would come from the operating system itself (e.g. from GetLastError() in Windows or errno in Linux).

In my opinion, the error message above is really useful to users and developers. The typical user can stop reading after one or two sentences and have a pretty good idea of what went wrong. The application's developer can read the whole thing and it's almost as good as having a stack trace.

My question: What is the right way to generate such an error message in Rust?

In C#, this is how I did it: the .NET Exception class has a Message property that returns one or more complete sentences describing the error and an InnerException property. As an exception propagates up the call stack, any function can catch it, wrap it inside a new exception using the InnerException property, and then throw the new exception. When the exception is caught by high-level GUI code, we have a linked list of exceptions tied together by the InnerException property. We can iterate through this list, check the types of the exceptions, and also concatenate their Message properties together to get a nice error message.

The Rust Error type has almost all the same machinery as .NET. The description method returns a string, and the cause method returns the low-level inner error. However, the big problem is that the documentation of description says:

The description should not contain newlines or sentence-ending punctuation, to facilitate embedding in larger user-facing strings.

I don't understand this design. Why does the lack of sentence-ending punctuation help when generating a user-facing error string? I just don't see how that would work; is there any Rust code in use today that actually concatenates the description of multiple errors together to make a user-facing string? How should I implement description if my error takes two or more sentences to describe? The official Microsoft error codes each are associated with an English sentence with a period at the end. If we want to embed those sentences in a Rust error, are we really supposed to strip off the period, and why would that be a good idea?

--David

3 Likes

Error::description is useful in very limited cases. If you have something like enum Error { TooMuchData, NotEnoughData, Unknown } then you can use description() to turn each of these cases into a a more friendly string, so you can print, “Your request failed because of a not enough data error,” instead of “Your request failed because of Error::NotEnoughData.”

When more dynamic information is need, use the Display or Debug traits to generate error output instead.

You might also find Cargo’s error-handling machinery useful for inspiration, since it includes "chained" error messages similar to the one in your example.

I wouldn't use description (and maybe even Display) for generating user-facing strings. The presentation layer can be very application specific (Do you need i18n? Perhaps you render rich Markdown/HTML text?), and plain strings might not be enough. Instead, I would try to store as much structured information (fields instead of strings) in Error structs as possible and bubble this to up the GUI level. At the GUI, I would log this raw error and convert it (via a giant match statement, perhaps) into GUI specific UserFacingError struct.

Somewhat similar discussion.

Thanks, @mbrubeck. I agree that Error::description is not very useful as it is currently designed.

I think that the Display and Debug traits would not work for my purpose, because the Rust standard library will emit a lot of errors that have those traits but they do not provide a string with complete sentences.

I will have to look at what Cargo is doing; thanks for the link. I glanced through that file briefly and I didn't see any examples of the kinds of error messages that are generated and whether they have complete sentences in them.

Thanks, @matklad. Yeah, maybe the GUI needs a giant match statement. That will be hard to maintain though, because you have to list all the errors from your entire tree of dependencies and know the details about how to convert them into English sentences. The .NET way I described in my first post was a lot simpler, and even though it didn't support the fancy features you are talking about, it helped produce very nice error messages.

Since the official Rust Error trait does attempt to provide user-facing plain-text English strings describing the errors, it seems strange that those strings are incomplete fragments of a sentence and there is no good way to show them to users.

Cargo's errors look like this:

error: Couldn't load Cargo configuration

Caused by:
  could not parse TOML configuration in `/home/mbrubeck/test/.cargo/config`

Caused by:
  could not parse input as TOML
/home/mbrubeck/test/.cargo/config:1:16-1:17 expected `=`, but found `:`

The formatting of these errors is done here and it uses the Display trait.

1 Like

Thanks, @mbrubeck. I don't like the style of that error message (inconsistent capitalization, use of colons and indentation), and I think it would look bad inside a message box in a GUI, but at least it is as usable as the example error messages I gave earlier.

In my Rust applications, I will probably ignore the Error::description function and just use Display::fmt. If Display::fmt makes something that ends with a period, then I will know it is one or more complete sentences and print it as is. If it does not end with a period, I will add a period and also prepend "Caused by:" to it.

Here is a hypothetical example where the first two errors were made by me and have complete sentences, and the second error came from a Rust library that doesn't follow my convention of making complete sentences in Display::fmt:

Failed to connect to the device. Unable to get the firmware version from the device. Caused by: could not parse input as TOML.

What do people think of that plan?

1 Like

I was kind of hoping to get more input from people on why the Rust Error trait is designed the way it is, how to generate nice error messages with complete sentences, and what people think of my plan above. (Bump.)

1 Like

@DavidEGrayson have you seen error-chain work? It should definitely help with, well, chaining errors on different layers. Not sure about message formatting though.