Make error messages actionable
Whenever possible, avoid making the user google the solution to your error.
Example:
File not found: config.tomlIf this is all the user gets, then they may have to go and look up the documentation of your program to learn about the config.toml file: Where to store it? What content to add?
Consider making it actionable:
Configuration file not found. Create a default config file with `--generate-defaults`. This will create a new default configuration file at `$XDG_CONFIG_HOME/myApp/config.toml`If it is so simple, maybe your tool can just generate the config file automatically when it is missing?
Consider the following ladder of effort:
- Inform: Tell the user what is wrong
- Action: Offer an actionable suggestion if there is a likely solution. e.g.
Try clearing the cache: rm -r .cache. - Auto-fix (opt-in): Offer an automated fix, e.g.
--fixoption on a static analyzer, if the action is automatable, but applying it requires a judgement call. Offer multiple alternatives if more than one fix may be valid and the choice depends on the user’s intent. - Auto-apply (by default): If the automatable fix is mechanical, safe and free of assumptions, then apply it automatically.
- e.g. automated formatting, automated schema upgrade, etc.
Whether an auto-fix is safe depends on the contract: if port is already in use occurs, then a dev server can try to pick another free port; a production service with a fixed, predictable port cannot.
No Assumptions
Section titled “No Assumptions”The message may only assume what it can verify at trigger-time.
For example, a check that stops at the syntaax may report:
std::lock_guard<std::mutex> lock{mutex}; ^~~~ warning: variable 'lock' declared but never usedBut it cannot verify that the variable is unnecessary. lock is never read, yet it exists for its destructor’s side effect (see RAII). A message prescribing “unused variable: remove it” would break mutual exclusion.
When intent cannot be verified, the remedy must not depend on it. Offer the alternatives instead of a single fix:
lock is never referenced. If this is intentional (for example, an RAII guard), no action is needed. Otherwise, remove it.Or don’t emit the message at all. GCC documents that for C++ types with non-trivial constructors and/or destructors, “it is impossible for the compiler to determine whether a variable of this type is truly unused if it is not referenced” — which is why such variables are exempt from -Wunused-variable by default.1