jdoctor

Project Url: melix/jdoctor
Introduction: A Java library for designing good error messages
More: Author   ReportBugs   
Tags:

Build Language grade: Java

Designing good error messages is hard. In Java, most often, developers just rely on throwing exceptions with a small text. In best cases, the error message is sufficiently understandable, but there are lots of cases where it's inefficient.

A good error message should describe:

  • how severe the problem is (can this be safely ignored?)
  • what problem was encountered. This is usually where developers stop. For example, "foo is null"
  • where the problem was encountered (in which context, e.g what file being compiled, what line number, ...)
  • why the problem happened

and eventually, a set of possible solutions to the problem.

Exceptions are not good at solving this, because an exception describes a failure in the program execution flow. Stack traces are good for debugging, but it's rare that a user cares where in your program a problem occured: most likely what they are interested in is how they can solve the problem.

As a consequence, JDoctor provides a library to make it easier to design good error messages, by modeling them properly.

This library is currently in experimental stages, feedback is welcome.

Example

This is an example of how you would construct a new problem descriptor using the convenience API:

ProblemBuilder.newBuilder(TasteProblem.INVALID_INGREDIENT, Severity.HIGH, "Hawaiian pizza")
    .withShortDescription("pineapple on pizza isn't allowed")
    .because("the Italian cuisine should be respected")
    .withLongDescription("Pineapple on pizza would put your relationship with folks you respect at risk.")
    .documentedAt("https://www.bbc.co.uk/bitesize/articles/z2vftrd")
    .addSolution(s -> s.withShortDescription("eat pineapple for desert"))
    .addSolution(s -> s.withShortDescription("stop adding pineapple to pizza"))

In practice, we encourage users to implement their own implementation of the Problem interface (for example extending the BaseProblem class) and come with topical builders which are specific to a category of problems.

For example:

problemRecorder.recordNewProblem(problem ->
    problem.forType(classWithAnnotationAttached)
        .reportAs(ERROR)
        .withId(ValidationProblemId.INVALID_USE_OF_CACHEABLE_TRANSFORM_ANNOTATION)
        .withDescription(() -> String.format("Using %s here is incorrect", getAnnotationType().getSimpleName()))
        .happensBecause(() -> String.format("This annotation only makes sense on %s types", TransformAction.class.getSimpleName()))
        .documentedAt("validation_problems", "invalid_use_of_cacheable_transform_annotation")
        .addPossibleSolution("Remove the annotation"))

Then you might wonder how this should be rendered. As always, the answer is "it depends". jdoctor is agnostic with regards to rendering. You will not render a message the same way if you have a CLI, a rich interface or an HTML report. In short: rendering is the responsibility of the consumer of the problems.

For example, you might want to collect multiple problems, sort them by category, then render a short message and tell that the full explanation is available in a report.

However, for convenience, jdoctor-utils provides a simple, simplistic renderer. Here's an example of output for the first example:

The cooking police arrested you: pineapple on pizza isn't allowed

Where? : Hawaiian pizza

Why? : the Italian cuisine should be respected

Details: Pineapple on pizza would put your relationship with folks you respect at risk.

Possible solutions:
    - eat pineapple for desert
    - stop adding pineapple to pizza

You can learn more about this problem at https://www.bbc.co.uk/bitesize/articles/z2vftrd

Modules

This project consists of the following modules published under the me.champeau.jdoctor group id:

  • jdoctor-core is the main API mosly consisting of the model
  • jdoctor-utils is a convenience module providing builders and renderers for problems and solutions
  • jdoctor-bom is a platform used for aligning dependency versions

Using the library in your favorite project

To use jdoctor with Gradle:

dependencies {
    implementation("me.champeau.jdoctor:jdoctor-core:0.1")
}

From Maven:

<dependencies>
    <dependency>
        <groupId>me.champeau.jdoctor</groupId>
        <artifactId>jdoctor-core</artifactId>
        <version>0.1</version>
    </dependency>
</dependencies>

Building

Run ./gradlew build

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools