Skip to content

Make error messages actionable

Whenever possible, avoid making the user google the solution to your error.

Example:

Terminal window
File not found: config.toml

If 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:

Terminal window
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:

  1. Inform: Tell the user what is wrong
  2. Action: Offer an actionable suggestion if there is a likely solution. e.g. Try clearing the cache: rm -r .cache.
  3. Auto-fix (opt-in): Offer an automated fix, e.g. --fix option 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.
  4. 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.

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 used

But 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

  1. GCC: C++-Specific Variable, Function, and Type Attributes ↩