Keywords: problems, api, problem

# Reporting Plugin Problems with the Problems API

version 9.6.1

Gradle’s Problems API lets plugins report rich, structured information about issues that occur during a failing build.

The following example shows how a plugin can report a problem:

ProblemReportingPlugin.java

    public class ProblemReportingPlugin implements Plugin<Project> {

        public static final ProblemGroup PROBLEM_GROUP = ProblemGroup.create("sample-group", "Sample Group");

        private final ProblemReporter problemReporter;

        interface SomeData extends AdditionalData {
            void setName(String name);
            String getName();
        }

        @Inject
        public ProblemReportingPlugin(Problems problems) { (1)
            this.problemReporter = problems.getReporter(); (2)
        }

        public void apply(Project project) {
            ProblemId problemId = ProblemId.create("adhoc-deprecation", "Plugin 'x' is deprecated", PROBLEM_GROUP);
            this.problemReporter.report(problemId, builder -> builder (3)
                .details("The plugin 'x' is deprecated since version 2.5")
                .solution("Please use plugin 'y'")
                .additionalData(SomeData.class, additionalData -> {
                    additionalData.setName("Some name"); (4)
                })
            );
        }
    }

1 | Inject the `Problem` service into the plugin.
2 | Create a `ProblemReporter` for the plugin.
3 | Report a recoverable problem so the build can continue.
4 | Add optional extra data to the problem.

## Problems API Usage

The following simple plugin adds a `greet` task:

- It prints a `Hello World` greeting when used correctly: `./gradlew greet -Precipient=World`

- It warns if no recipient is set: `./gradlew greet`

- It fails if the recipient is `fail`: `./gradlew greet -Precipient=fail`

The details of the failure will be provided in the HTML Problems Report:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt

    package org.example

    import org.gradle.api.DefaultTask
    import org.gradle.api.GradleException
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    import org.gradle.api.provider.Property
    import org.gradle.api.problems.*
    import org.gradle.api.tasks.Input
    import org.gradle.api.tasks.TaskAction
    import javax.inject.Inject

    /**
     * Simple demo plugin for the Gradle Problems API.
     */
    abstract class HelloProblemsPlugin : Plugin<Project> {
        override fun apply(project: Project) {
            project.tasks.register("greet", GreetTask::class.java) {
                group = "demo"
                description = "Greets a recipient. Demonstrates Problems API."
            }
        }
    }

    /**
     * Extra structured data to attach to problem reports.
     * Tools like Build Scans or IDEs could show this.
     */
    interface GreetProblemData : AdditionalData {
        var configuredRecipient: String?
    }

    /**
     * Custom task that uses the Gradle Problems API.
     */
    abstract class GreetTask : DefaultTask() {

        @get:Input
        abstract val recipient: Property<String>

        @get:Inject
        abstract val problems: Problems

        private val GROUP: ProblemGroup =
            ProblemGroup.create("org.example.hello-problems", "Hello Problems")
        private val WARN_ID: ProblemId =
            ProblemId.create("missing-recipient", "Recipient not set", GROUP)
        private val FAIL_ID: ProblemId =
            ProblemId.create("forbidden-recipient", "Forbidden recipient 'fail'", GROUP)

        @TaskAction
        fun run() {
            val reporter = problems.reporter
            val name = recipient.orNull?.trim().orEmpty()

            // Warning: missing recipient -> provide a helpful suggestion
            if (name.isEmpty()) {
                reporter.report(WARN_ID) {
                    details("No recipient configured")
                    severity(Severity.WARNING)
                    solution("""Set the recipient: tasks.greet { recipient = "World" }""")
                    documentedAt("https://gradle.org/hello-problems#recipient")
                    additionalData(GreetProblemData::class.java) {
                        configuredRecipient = null
                    }
                }
            }

            // Fatal: a specific value is disallowed to show throwing()
            else if (name.equals("fail", ignoreCase = true)) {
                throw reporter.throwing(GradleException("forbidden value"), FAIL_ID) {
                    details("Recipient 'fail' is not allowed")
                    severity(Severity.ERROR)
                    solution("""Choose another value, e.g. recipient = "World".""")
                    documentedAt("https://gradle.org/hello-problems#forbidden")
                    additionalData(GreetProblemData::class.java) {
                        configuredRecipient = name
                    }
                }
            }

            logger.lifecycle("Hello, $name!")
        }
    }

plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy

    package org.example

    import org.gradle.api.DefaultTask
    import org.gradle.api.GradleException
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    import org.gradle.api.provider.Property
    import org.gradle.api.problems.*
    import org.gradle.api.tasks.Input
    import org.gradle.api.tasks.TaskAction
    import javax.inject.Inject

    /**
     * Simple demo plugin for the Gradle Problems API.
     */
    class HelloProblemsPlugin implements Plugin<Project> {
        @Override
        void apply(Project project) {
            project.tasks.register("greet", GreetTask) { task ->
                task.group = "demo"
                task.description = "Greets a recipient. Demonstrates Problems API."
            }
        }
    }

    /**
     * Extra structured data to attach to problem reports.
     * Tools like Build Scans or IDEs could show this.
     */
    interface GreetProblemData extends AdditionalData {
        String getConfiguredRecipient()
        void setConfiguredRecipient(String value)
    }

    /**
     * Custom task that uses the Gradle Problems API.
     */
    abstract class GreetTask extends DefaultTask {

        @Input
        abstract Property<String> getRecipient()

        @Inject
        abstract Problems getProblems()

        private static final ProblemGroup GROUP =
            ProblemGroup.create("org.example.hello-problems", "Hello Problems")
        private static final ProblemId WARN_ID =
            ProblemId.create("missing-recipient", "Recipient not set", GROUP)
        private static final ProblemId FAIL_ID =
            ProblemId.create("forbidden-recipient", "Forbidden recipient 'fail'", GROUP)

        @TaskAction
        void run() {
            def reporter = problems.reporter
            def name = recipient.orNull?.trim() ?: ""

            // Warning: missing recipient -> provide a helpful suggestion
            if (name.isEmpty()) {
                reporter.report(WARN_ID) { spec ->
                    spec.details("No recipient configured")
                        .solution('Set the recipient: tasks.greet { recipient = "World" }')
                        .documentedAt("https://gradle.org/hello-problems#recipient")
                        .additionalData(GreetProblemData) {
                            it.configuredRecipient = null
                        }
                }
            }
            // Fatal: a specific value is disallowed to show throwing()
            else if (name.equalsIgnoreCase("fail")) {
                throw reporter.throwing(new GradleException("forbidden value"), FAIL_ID) { spec ->
                    spec.details("Recipient 'fail' is not allowed")
                        .solution('Choose another value, e.g. recipient = "World".')
                        .documentedAt("https://gradle.org/hello-problems#forbidden")
                        .additionalData(GreetProblemData) {
                            it.configuredRecipient = name
                        }
                }
            }

            logger.lifecycle("Hello, $name!")
        }
    }

How it works:

1. Injects the `Problems` service into the task.

2. Creates a `ProblemGroup` to group related problems.

3. Creates a `ProblemId` for each distinct issue.

4. Reports warnings with the `ProblemReporter.report()` when the build can continue.

5. Throws structured failures with `ProblemReporter.throwing()` when the build must stop.

6. Optionally attaches `AdditionalData` to `ProblemSpec` so advanced tools can show extra info.

### Injecting the `Problems` service

Gradle will automatically provide the `Problems` service when injected into the custom task type:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt

    @get:Inject
    abstract val problems: Problems

plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy

    @Inject
    abstract Problems getProblems()

### Creating a `ProblemId` and `ProblemGroup`

Problem IDs and Groups help Gradle deduplicate, summarize, and link to solutions:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt

    private val GROUP: ProblemGroup =
        ProblemGroup.create("org.example.hello-problems", "Hello Problems")
    private val WARN_ID: ProblemId =
        ProblemId.create("missing-recipient", "Recipient not set", GROUP)
    private val FAIL_ID: ProblemId =
        ProblemId.create("forbidden-recipient", "Forbidden recipient 'fail'", GROUP)

plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy

    private static final ProblemGroup GROUP =
        ProblemGroup.create("org.example.hello-problems", "Hello Problems")
    private static final ProblemId WARN_ID =
        ProblemId.create("missing-recipient", "Recipient not set", GROUP)
    private static final ProblemId FAIL_ID =
        ProblemId.create("forbidden-recipient", "Forbidden recipient 'fail'", GROUP)

### Choosing a reporting mode for the `ProblemReporter`

Plugins can report problems in two ways:

- `report()` — use for recoverable problems when the build should continue.

- `throwing()` — use for non-recoverable problems that should fail the build.

Once you have decided what type of problem you want to report, get the `ProblemReporter` using `problem.getReporter()`:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt

    val reporter = problems.reporter

plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy

    def reporter = problems.reporter

#### Reporting a recoverable problem

Use `problems.getReporter().report {}` to signal something went wrong but allow the build to continue:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt

    reporter.report(WARN_ID) {
        details("No recipient configured")
        severity(Severity.WARNING)
        solution("""Set the recipient: tasks.greet { recipient = "World" }""")
        documentedAt("https://gradle.org/hello-problems#recipient")
        additionalData(GreetProblemData::class.java) {
            configuredRecipient = null
        }
    }

plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy

    reporter.report(WARN_ID) { spec ->
        spec.details("No recipient configured")
            .solution('Set the recipient: tasks.greet { recipient = "World" }')
            .documentedAt("https://gradle.org/hello-problems#recipient")
            .additionalData(GreetProblemData) {
                it.configuredRecipient = null
            }
    }

#### Reporting a fatal problem

Use `problems.getReporter().throwing {}` to fail the build with a structured error:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt

    throw reporter.throwing(GradleException("forbidden value"), FAIL_ID) {
        details("Recipient 'fail' is not allowed")
        severity(Severity.ERROR)
        solution("""Choose another value, e.g. recipient = "World".""")
        documentedAt("https://gradle.org/hello-problems#forbidden")
        additionalData(GreetProblemData::class.java) {
            configuredRecipient = name
        }
    }

plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy

    throw reporter.throwing(new GradleException("forbidden value"), FAIL_ID) { spec ->
        spec.details("Recipient 'fail' is not allowed")
            .solution('Choose another value, e.g. recipient = "World".')
            .documentedAt("https://gradle.org/hello-problems#forbidden")
            .additionalData(GreetProblemData) {
                it.configuredRecipient = name
            }
    }

This throws an exception tied to a rich problem report so Gradle clients can display meaningful failure info.

### Add data using `ProblemSpec`

When reporting a problem, you can provide a wide variety of metadata such as descriptions, severity, solutions, and additional data:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt

    details("No recipient configured")
    severity(Severity.WARNING)
    solution("""Set the recipient: tasks.greet { recipient = "World" }""")
    documentedAt("https://gradle.org/hello-problems#recipient")
    additionalData(GreetProblemData::class.java) {
        configuredRecipient = null
    }

plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy

    spec.details("No recipient configured")
        .solution('Set the recipient: tasks.greet { recipient = "World" }')
        .documentedAt("https://gradle.org/hello-problems#recipient")
        .additionalData(GreetProblemData) {
            it.configuredRecipient = null
        }

See `ProblemSpec` for all available fields.

## Problem Summarization

Gradle avoids spamming users with duplicate problem messages:

- The first few instances of a problem are reported individually as `Problem` events.

- Subsequent duplicates are summarized at the end of the build as a `ProblemSummary`, delivered with a `ProblemSummariesEvent`.

## Build Failures

The standard way to fail a build is by throwing an exception. The Problems API enhances this by allowing you to throw exceptions tied to detailed problem reports:

FailingTask.java

    ProblemId id = ProblemId.create("sample-error", "FailingTask execution problem", StandardPlugin.PROBLEM_GROUP);
    throw getProblems().getReporter().throwing((new RuntimeException("Message from runtime exception")), id, problemSpec -> {
        problemSpec.contextualLabel("This happened because ProblemReporter.throwing() was called")
            .details("This is a demonstration of how to add\ndetailed information to a build failure")
            .documentedAt("https://example.com/docs")
            .solution("Remove the Problems.throwing() method call from the task action");
    });

This ensures build failures are clearly associated with their underlying issues and are visible across all Gradle clients (CLI, Build Scan, IDEs, Tooling API).

### Command-line Interface

Gradle CLI output shows detailed information from the reported problem:

    FAILURE: Build failed with an exception.

    * What went wrong:
    Execution failed for task ':myFailingTask'.
    > FailingTask execution problem  (1)
        This happened because ProblemReporter.throwing() was called  (2)
          This is a demonstration of how to add  (3)
          detailed information to a build failure

    * Try:  (4)
    > Remove the Problems.throwing() method call from the task action
    > Run with --scan to generate a Build Scan (powered by Develocity).
    BUILD FAILED in 0ms  (5)

1 | The display name of the problem ID.
2 | The contextual label (or the supplied exception’s message if the contextual label is missing).
3 | The content of the `details` property.
4 | The solutions or recommended actions.
5 | The `AdditionalData` is not shown in CLI output.

### Tooling API Clients

Tooling API clients can access reported problems tied to failures:

- Register a progress listener for `OperationType.ROOT`.

- Check if the result is a `FailureResult` and retrieve problems with `Failure.getProblems()`.

Alternatively, configure the project connection with `LongRunningOperation.withFailureDetails()`. Then failure details, including problems, are automatically available via `GradleConnectionException.getFailures()`.

## Generated HTML Report

Gradle generates an HTML report at the end of the build if any problems were reported. It’s a central place to review issues and complements the console and Build Scan outputs.

- The report is skipped if no problems are found.

- Disable report generation with the `--no-problems-report` flag.

Console output includes a link to the report:

    [Incubating] Problem report is available at: <project-dir>/build/reports/problems/problems-report.html

    BUILD SUCCESSFUL in 1s

`AdditionalData` is not included in the HTML report. Plugins can log their own problem events, which appear alongside Gradle’s built-in reports.

## Console rendering

All problem reports are now rendered in the console output if `--warning-mode=all` is configured.