As an open-source maintainer, I always put effort into documenting all known edge cases so that users know how to fix problems. So, whenever users report incompatibilities, we highlight them in our documentation. Still, I realized this approach wasn’t working when users came to our Slack asking for help with problems we had already documented.
As project maintainers, we tend to be overly optimistic about how good the documentation is. But the target metric should not be how detailed our documentation is but how fast users can get things done. And when things go wrong, reading the documentation is not always the quickest route, so don’t make your users read your docs, help them right on the spot.
A few weeks ago, a user reported an issue. The details are not important, but it required us to add a new argument to a class. We added the argument to the constructor, documented it, and posted the solution in the GitHub issue; however, when thinking about what would happen if a new user had the same issue, I realized we solved the problem for one user but not the rest. Most likely, other users would have a hard time trying to fix the issue, and most likely, they’d give up if they didn’t find the answer quickly.
Useful error messages
A helpful error message tells you three things:
- What failed
- Why it failed
- How to fix it
RuntimeError: Cannot re-initialize CUDA in forked subprocess. To use CUDA with multiprocessing, you must use the ‘spawn’ start method
This error message contains the three elements:
- Cannot re-initialize CUDA [What failed]
- …in forked process [Why it failed]
- Use the ‘spawn’ start method [How to fix it]
The problem is that our framework builds an abstraction, so users don’t have to use the
multiprocessing module directly; hence, the user couldn’t fix the issue unless they modified the source code.
In our specific use case, here’s a better error message:
RuntimeError: Cannot re-initialize CUDA in forked subprocess. To use CUDA with multiprocessing, Pass ‘spawn’ to the ‘start_method’ argument of the Parallel executor constructor
Let’s see how to achieve this.
Helpful error messages
Note: the following sections contain Python code snippets, but the idea applies to any language.
We want to anticipate the error and tell the user how to get things running:
from some_package.exceptions import SomeException def thing_that_breaks(argument): ... def thing_that_the_user_calls(argument): try: thing_that_breaks(argument=argument) except SomeException as e: # add more context and raise whatever exception type makes sense raise RuntimeError('How to fix it') from e except: # raise the original exception, unmodified raise ...
raise exception from another_exception expression is called a chained exception in Python.
The previous snippet will provide the user-specific instructions when encountering the problem using our software.
However, we’re assuming that:
- We can import
some_package.exceptionsin our project’s codebase (which implies adding it as a dependency)
- We are sure that when
SomeExceptionis raised, the solution is what we are displaying to the user
Sometimes exceptions are too general, so we need to dig deeper. In such cases, we can use the error message as a proxy:
def thing_that_breaks(argument): ... def thing_that_the_user_calls(argument): try: thing_that_breaks(argument=argument) except Exception as e: if 'some hint' in str(e): raise Exception('Instructions on how to fix it') from e else: raise ...
There are obvious drawbacks to this approach: the error message might change, although the same is true for the exception type, so in any case, ensure you have unit tests in place.
I’ve encountered cases where checking the error message isn’t enough, and we might display inaccurate instructions. In such situations, I write an error to reflect that:
If having issues with X, try [possible solution]