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> {
private final ProblemReporter reporter;
private final ProblemId pluginMisconfigured;
interface SomeData extends AdditionalData {
void setName(String name);
String getName();
}
@Inject
public ProblemReportingPlugin(Problems problems) { (1)
reporter = problems.getReporter();
pluginMisconfigured = problems.getGroups().getGradle().getBuildDefinition().problemId("Plugin 'x' is misconfigured"); (2)
}
public void apply(Project project) {
reporter.report(pluginMisconfigured, builder -> builder (3)
.details("The property 'outputDirectory' of plugin 'x' is not set")
.solution("Set 'outputDirectory' in the 'x' extension")
.additionalData(SomeData.class, additionalData -> {
additionalData.setName("Some name"); (4)
})
);
}
}
| 1 | Inject the Problems service into the plugin. |
| 2 | Create the ProblemId once, in one of the predefined problem groups, and keep it together with the reporter; both are stable for the lifetime of 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 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 @Inject constructor(private val problems: Problems) : DefaultTask() {
@get:Input
abstract val recipient: Property<String>
private val problemGroup = problems.groups.others.group("Hello Problems")
private val warnId = problemGroup.problemId("Recipient not set")
private val failId = problemGroup.problemId("Forbidden recipient 'fail'")
@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(warnId) {
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"), failId) {
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()
private final Problems problems
private final ProblemId warnId
private final ProblemId failId
@Inject
GreetTask(Problems problems) {
this.problems = problems
def problemGroup = problems.groups.others.group("Hello Problems")
warnId = problemGroup.problemId("Recipient not set")
failId = problemGroup.problemId("Forbidden recipient 'fail'")
}
@TaskAction
void run() {
def reporter = problems.reporter
def name = recipient.orNull?.trim() ?: ""
// Warning: missing recipient -> provide a helpful suggestion
if (name.isEmpty()) {
reporter.report(warnId) { 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"), failId) { 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:
-
Injects the
Problemsservice into the task. -
Picks a
ProblemGroupfrom the predefined hierarchy, creating its own subgroup for the plugin’s problems, and creates aProblemIdin that group for each distinct issue. -
Reports warnings with
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, here through its constructor:
abstract class GreetTask @Inject constructor(private val problems: Problems) : DefaultTask() {
private final Problems problems
private final ProblemId warnId
private final ProblemId failId
@Inject
GreetTask(Problems problems) {
this.problems = problems
def problemGroup = problems.groups.others.group("Hello Problems")
warnId = problemGroup.problemId("Recipient not set")
failId = problemGroup.problemId("Forbidden recipient 'fail'")
}
Choosing a ProblemGroup and creating a ProblemId
Every problem belongs to a ProblemGroup, and is identified by a ProblemId within that group.
Groups and IDs let Gradle deduplicate and summarize problems, and let consumers such as IDEs navigate and filter them.
Gradle provides a predefined hierarchy of groups, available from the Problems service as problems.groups.
Pick the group matching what the problem is about, or create a subgroup for your plugin below the closest predefined group, then create the problem ID from the group.
Groups and IDs are stable values: create them once, for example in the constructor of your plugin, and reuse them at every report site.
private val problemGroup = problems.groups.others.group("Hello Problems")
private val warnId = problemGroup.problemId("Recipient not set")
private val failId = problemGroup.problemId("Forbidden recipient 'fail'")
def problemGroup = problems.groups.others.group("Hello Problems")
warnId = problemGroup.problemId("Recipient not set")
failId = problemGroup.problemId("Forbidden recipient 'fail'")
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(warnId) {
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(warnId) { 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:
throw reporter.throwing(GradleException("forbidden value"), failId) {
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"), failId) { 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:
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")
.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.
Predefined Problem Groups
Gradle provides a predefined hierarchy of problem groups through ProblemGroups, available from the Problems service as problems.groups.
Groups describe what a problem is about, not where or when it happened; the location of a problem is recorded separately.
A shared set of groups keeps problem reports consistent across plugins, so that build engineers, IDEs, and other tools can navigate, filter, and aggregate problems reliably.
The hierarchy
The hierarchy has three levels, with these kinds of groups:
-
Root groups only structure the hierarchy. Problems cannot be reported directly into a root group.
-
Subgroups, either predefined or created by a plugin, hold problems and can have one further level of subgroups.
-
Third-level groups, created by a plugin below a subgroup, hold problems and cannot be extended.
-
Gradle’s own subgroups hold problems; plugins cannot create subgroups below them.
-
Undefinedgroups hold problems and can never be extended.
Every root group and every extensible subgroup also has a synthetic Undefined subgroup, for problems that do not fit any of its other subgroups.
The names shown below identify a group together with its parent.
group(name) matches on the name, and console output renders groups as a chain such as Gradle > Build Cache.
| Root group | Predefined subgroups | What it covers |
|---|---|---|
Gradle |
Build Cache, Build Definition, Build Logic, Configuration Cache, Deprecation (reserved for Gradle), DSL Evaluation, Invocation, Isolated Projects, Plugin Validation |
Initialization, configuration, and execution of the Gradle runtime, integration of built-in and third-party plugins with Gradle. This group is closed: plugins can report into its subgroups but cannot add subgroups. The |
Dependencies |
Declaration, Locking, Graph Resolution, Artifact Resolution, Verification |
Declaration, resolution, locking, and verification of dependencies required by projects or the Gradle runtime. |
Transformation |
Binary Transformation, Code Transformation, Resource Transformation |
Generation or manipulation of code, binaries, or resources before running or packaging. |
Compilation |
C++, Groovy, Java, Kotlin, Scala, Swift |
Code compilation, including the compiler’s configuration, compiler invocation, or compiler plugins. |
External Processes |
Application, Service, Tool |
External process invocations of compiled applications, native tools, and other runnable artifacts. |
Packaging |
Distributions, JAR, Native Linking, Signing |
Assembling products into various runnable and deployable forms, including creating binaries, archives, installers, or container images, and signing them. |
Verification |
Code Coverage, Code Quality, Security, Testing |
Verification of software correctness, quality, and compliance through testing, static analysis, code coverage, linting, and other verification mechanisms that ensure the software meets specified requirements and standards, including setup and configuration of required tools and frameworks. |
Documentation |
Javadoc, Groovydoc, Scaladoc |
Production of documentation of any form, including API docs, manuals, or websites. |
Provisioning |
Infrastructure, Tools and Toolchains |
Making resources available for use and managing them, including tools and toolchains, services, or infrastructure. |
Delivery |
Publishing, Deployment |
Making products available to users, including publishing software components, deploying applications, or uploading documentation. |
Others |
none |
Problems that do not fit in any other group. |
Each predefined group carries a description, documented in the Javadoc of ProblemGroups and its group types.
Choosing a group
To decide where a problem belongs, consider what the code reporting it is doing and what the problem communicates, then:
-
Find the root group matching that activity. For example, a problem found while compiling Java code belongs under
compilation. -
If a predefined subgroup matches, report the problem there:
problems.groups.compilation.java.problemId("Unused import"). -
If no predefined subgroup matches, create a subgroup for your plugin below the root group, or use the root group’s
undefinedsubgroup:problems.groups.transformation.group("KMP").problemId("Bundle failed"). -
If no root group matches, create a subgroup below
others, or useproblems.groups.others.undefined.
When several groups match, pick the one closest to the context of your plugin.
For example, a task that processes files for documentation reports under documentation rather than transformation, and a problem about a task’s input annotations reports under gradle.pluginValidation even though it surfaces at execution time.
Creating subgroups
Subgroups are created from an existing group with group(name), and problem IDs from a group with problemId(name).
The types of the hierarchy enforce the placement rules: a root group only offers group(name), a subgroup offers both, and the closed kinds (third-level groups, Gradle’s own subgroups, and Undefined groups) only offer problemId(name).
Names are case-sensitive.
Calling group(name) with the exact name of a predefined subgroup returns that predefined group; any other name creates a new group, even if it differs from a predefined one only by case: problems.groups.compilation.group("Java") is the predefined Java subgroup, problems.groups.compilation.group("java") is a separate group.
The name Undefined is reserved in any letter case; use the undefined property instead.
Group names are limited to 50 characters and consist of letters and digits of any script, single spaces between words, and the characters -, _, ., `, `/`, `&`, `'` and `#`.
They start with a letter or a digit, and must be in Unicode normalization form NFC, so that two names that look the same are the same name.
Symbols, quotes and other punctuation are rejected; examples of valid names are `C+, CI/CD, Tools & Toolchains, Русский or 中文.
Problem names are sentences and are less restricted: they must not be blank, must not have leading or trailing whitespace, must not contain control or format characters (other than the zero width joiner and non-joiner), and are limited to 2000 characters.
Keep group names short and noun-based, so they read well as categories in a report. Formulate problem names as a single, concise sentence describing the problem, without embedding contextual details such as file names or values; those belong in the problem’s label, details, and locations.
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", "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
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.
|
To keep stack trace capture affordable, Gradle infers a source location for only a limited number of problems per build, so very large builds may list some problems without a file and line.
Run with |
-
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.
Console rendering
All problem reports are now rendered in the console output if --warning-mode=all is configured.