Reporting Plugin Problems with the Problems API
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:
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'")
.severity(Severity.WARNING)
.additionalData(SomeData.class, additionalData -> {
additionalData.setName("Some name"); (4)
})
);
}
}
| 1 | Inject the Problem service into the plugin. |
| 2 | Create a ProblemReporter for the plugin (use the plugin ID as the namespace). |
| 3 | Report a recoverable problem so the build can continue. |
| 4 | Add optional extra data to the problem. |
See the full end-to-end sample for more details.
Problems API Usage
The following simple plugin adds a greet task:
-
It prints a
Hello Worldgreeting 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:
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!")
}
}
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")
.severity(Severity.WARNING)
.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")
.severity(Severity.ERROR)
.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:
-
Injects the
Problemsservice into the task. -
Creates a
ProblemGroupto group related problems. -
Creates a
ProblemIdfor each distinct issue. -
Reports warnings with the
ProblemReporter.report()when the build can continue. -
Throws structured failures with
ProblemReporter.throwing()when the build must stop. -
Optionally attaches
AdditionalDatatoProblemSpecso advanced tools can show extra info.
Injecting the Problems service
Gradle will automatically provide the Problems service when injected into the custom task type:
@get:Inject
abstract val problems: Problems
@Inject
abstract Problems getProblems()
Creating a ProblemId and ProblemGroup
Problem IDs and Groups help Gradle deduplicate, summarize, and link to solutions:
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)
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():
val reporter = problems.reporter
def reporter = problems.reporter
Reporting a recoverable problem
Use problems.getReporter().report {} to signal something went wrong but allow the build to continue:
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
}
}
reporter.report(WARN_ID) { spec ->
spec.details("No recipient configured")
.severity(Severity.WARNING)
.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:
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
}
}
throw reporter.throwing(new GradleException("forbidden value"), FAIL_ID) { spec ->
spec.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) {
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:
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
}
spec.details("No recipient configured")
.severity(Severity.WARNING)
.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
Problemevents. -
Subsequent duplicates are summarized at the end of the build as a
ProblemSummary, delivered with aProblemSummariesEvent.
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:
ProblemId id = ProblemId.create("sample-error", "Sample Error", 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 ':sample-project:myFailingTask'.
> Message from runtime exception
This happened because ProblemReporter.throwing() was called
This is a demonstration of how to add
detailed information to a build failure
* Try:
> 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
Notes:
- AdditionalData is not shown in CLI output.
- Solutions or recommended actions in the report will appear in the “Try:” section.
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
FailureResultand retrieve problems withFailure.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-reportflag.
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.