I've written and maintain several Rust applications of substantial complexity.
When I began, I decided to use String in the "error channel" (as we Scala programmers call the monad "error" type parameter), as a punt.
What I've discovered is that it's entirely sufficient. No, I don't have fine-grained (or any) error categorization, but, in practice, the origin of the error coupled with an appropriate message (typically, in application layers, this is a lower-level error message with additional information) is sufficient to diagnose any error.
Thus, I have back-burnered my plans to switch to Anyhow.
So, I wish to ask, am I missing something? Is there an anyhow (or other) evangelist that would like to argue that using String errors is poor practice?
From an end-user perspective: I'd have to see more detail about how good you are about building strings with context to comment on what you might improve. But which of these categories are you closer to?
Error: invalid digit found in string
Error: error reading `Blocks.txt`
Caused by:
0: invalid Blocks.txt data on line 223
1: one end of range is not a valid hexidecimal integer
2: invalid digit found in string
From a programmer perspective: Are you an upstream library for anyone? Because if you use Strings, logic based on the exact error is painful and prone to breakage. For example trying to read a file that doesn't exist might result in a particular io::Error which isn't necessarily an application error (like insufficient filesystem permissions or an actual i/o reading error would be).
First and foremost, if you control both the error producer and the error consumer, and if returning a String is sufficient for your needs, then return a String and feel no qualms about it. Principles are nice, but practice counts too.
However, I would consider the following risk areas:
If you intend to do something with errors besides printing them, then String errors require that the caller know the exact format of the expected error string for each error they care about. That makes the format part of your API. If you find yourself in that situation, I'd consider switching to an enum of possible errors, whose fields expose that detail, instead.
A single String makes it tricky to add context information. @quinedot's reply covers this already but it's worth emphasizing: errors are also a user interface, and the user ultimately needs to be able to figure out what to do in order to address the error.
If you are shipping this to crates.io, or otherwise expecting error consumers to follow semver semantics when consuming your code, then you are effectively locking the error handling system in stone until you release a major version. That may or may not be something you're prepared for. You may wish to use this conversion; Result<T, Box<dyn Error>> is a very common alternative to anyhow and is less restrictive in this way.
No libs, except my own. I control everything.
And I make sure that at the spot an error message is created, every possible piece of useful information is included.
I only use String as error type when prototyping stuff, never in production code.
Usually I write my own error types, mostly enums with appropriate variants for each error category:
I am one of these guys who mostly implements everything by hand and avoids libraries like thiserror.
If possible and if it makes sense, I also just use an existing error type from the standard library or from a third-party library that I'm already using:
The reason why I'm using distinct types for error handling, especially in libraries I write is, so that the library user can make use of Rust's type system to handle different kinds of errors rather than relying on parsing strings.
In some cases, I had to put data in the returning error, and using only a String would have been counter-productive. In one instance, the data was moved into a function to create a different data, but in case of an error, it had to return the initial data in the error type so that the caller could get it back for another use. It would have worked with a clone and a simple string error, but it would have been costly.
In another case, similarly to what others mentioned earlier, I had a category of errors to report in a common log, and I wanted all the errors to carry easily identifiable information like the class of error (note, warning, fatal), a line number, an identifier, and a description, in order to sort and filter them out before presentation. I could have parsed that from a string, but it would have been harder.
It's less obvious when one owns the whole application, for sure. But if you write a library that has multiple layers and share it with others, perhaps using Error makes sense to encapsulate the levels and let the user retrieve them with Error::source if necessary.
Or the user of your code may find it very helpful to be able to write a unique implementation of From<YourError> for TheirError when they have to convert the errors from multiple sources into their own error type. Imagine if they had several 3rd-party functions potentially returning an error at the same place, but all returning only a string. They'd have to map the errors manually after every single call (making for a messy code) instead of using global Froms and a bunch of ?s.
PS: Those are just examples of when I think something else than a string is interesting. It doesn't mean using a string is bad. In many cases, it's perfectly fine IMO.