Debuggability
Users of your library will build on its functionality, and the features they build will contain errors that need to be identified and resolved. This error resolution process might be conducted within a debugger during development or using logging and observability tools in production. Your library can follow these best practices to make debugging it easier.
Provide a toString method for stateful types
For every type that contains state, provide a meaningful toString
implementation. This implementation should return an intelligible representation of the instance's current content, even for internal types.
Since toString
representations of types are often written to logs, consider security when implementing this method and avoid returning sensitive user data.
Ensure the format used to describe the state is as consistent as possible across the different types in your library. This format should be explicitly described and thoroughly documented when it is part of a contract implemented by your API. The output from your toString
methods may support parsing, for example in automated test suites.
For example, consider the following types from a library supporting service subscriptions:
Without a toString
method, printing a SubscriptionResult
instance is not very useful:
Nor is the information readily displayed in the debugger:
Adding a simple toString
implementation improves the output significantly in both cases:
While it might be tempting to use data classes to gain a toString
method automatically, it's not recommended for backward compatibility reasons. Data classes are discussed in more detail in the Avoid using data classes in your API section.
Note that the state described in the toString
method does not need to be information from the problem domain. It can relate to the status of ongoing requests (as in the example above), the health of connections to external services, or intermediate state within an ongoing operation.
For example, consider the following builder type:
This is how you would use this type:
If you halt the code at the breakpoint displayed on the image above, the information displayed will not be helpful:
Adding a simple toString
implementation results in a much more helpful output:
With this addition, the debugger shows:
This way, you can immediately see which fields are set and which are not.
Adopt and document a policy for handling exceptions
As discussed in the Choose appropriate error handling mechanism section, there are occasions when it's appropriate for your library to throw an exception to signal an error. You may create your own exception types for this purpose.
Libraries that abstract and simplify low-level APIs will also need to handle exceptions thrown by their dependencies. A library might choose to suppress the exception, pass it on as it is, convert it to a different type of an exception, or signal the error to users in a different way.
Any of these options could be valid, depending on the context. For example:
If a user adopts library A purely for the convenience of simplifying library B, it may be appropriate for library A to rethrow any exceptions generated by library B without modification.
If library A adopts library B purely as an internal implementation detail, then library-specific exceptions thrown by library B should never be exposed to users of library A.
You must adopt and document a consistent approach to exception handling so users can make productive use of your library. This is especially important for debugging. Users of your library should be able to recognise, in the debugger and in logs, when an exception has originated from your library.
The type of the exception should indicate the type of the error, and the data in the exception should help the user locate the root cause of the issue. A common pattern is to wrap a low-level exception in a library-specific one, with the original exception accessible as the cause
.
Next step
In the next part of the guide, you'll learn about testability.