skip to content

Reporting Structured Problems

Injecting the Problems service and reporting an identified problem with a contextual label and details. Asked to see whether you think about who has to read your plugin's error messages.

on this pageshow

questions

6

What is Gradle's Problems API and why would a plugin author report a structured problem instead of just throwing an exception or logging a warning?

level: juniorimportance: must knowfreq 35%

answer

  1. structured vs free-text logging
  2. many consumers: console, HTML, IDE, scan
  3. ProblemId + contextualLabel + details + severity
  4. injected Problems service
  5. report() vs throwing()

basics

~20 s

The Problems API lets a plugin report build issues as structured data (an ID, a message, severity, location) instead of plain text. Gradle then surfaces them uniformly in the console, the HTML report, IDEs, and the Tooling API.

solid answer

~40 s

Introduced as an incubating service, the **Problems API** is Gradle's structured channel for emitting diagnostics. Instead of a free-text `logger.warn(...)` or a raw exception, a plugin injects the `Problems` service and calls `problems.getReporter().report(id) { ... }`, attaching a stable `ProblemId`, a `contextualLabel`, `details`, severity, location, and suggested solutions. The value is that the same problem is then consumed by **multiple front-ends**: the console summary, the generated HTML problems report, IDEs via the **Tooling API** (so they can show inline annotations), and build scans. Logging is unstructured and lost across consumers; exceptions abort the build and carry no machine-readable category. Structured problems are categorizable, deduplicable, and actionable, and you can report them *without* failing the build (`report`) or *while* failing it (`throwing`).

code

kotlin · 13 lines
kotlin
abstract class LintPlugin @Inject constructor(
    private val problems: Problems
) : Plugin<Project> {
    override fun apply(project: Project) {
        val group = ProblemGroup.create("lint", "Lint checks")
        val id = ProblemId.create("naming", "Bad naming", group)
        problems.reporter.report(id) { spec ->
            spec.contextualLabel("Class 'foo' should be capitalized")
                .details("Type names must start with an uppercase letter.")
                .severity(Severity.WARNING)
        }
    }
}

go deeper

for a junior

Know it exists, that it's structured rather than free text, and that you inject the Problems service and call report().

for a middle

Explain the multi-consumer fan-out (console/HTML/IDE/scan) and the report vs throwing distinction.

for a senior

Discuss when to migrate existing warnings to structured problems and how stable IDs enable dedup/aggregation.

for a principal

Frame it as the org-wide diagnostics contract: stable taxonomy of ProblemGroups across plugins so dashboards and IDEs stay consistent.

## The problem with logging and exceptions For years a Gradle plugin had two ways to tell a user something was wrong: - `logger.warn("...")` / `logger.error("...")` — free text. It scrolls past in the console, is invisible to IDEs, and has no identity you can group or count. - `throw GradleException("...")` — aborts the build, but again is just a string with a stack trace. Neither is *machine-readable*. An IDE can't turn a warning into an inline annotation; a build scan can't say "this same deprecation fired 412 times"; a CI dashboard can't categorize failures. ## What the Problems API is The **Problems API** (package `org.gradle.api.problems`, incubating in Gradle 8.x) is a structured reporting service. A problem carries: - a **`ProblemId`** — a stable identity (a `name` + human `displayName`) that lives inside a **`ProblemGroup`** (a namespace, e.g. your plugin), - a **`contextualLabel`** — the short, situation-specific headline, - **`details`** — longer explanatory text, - a **severity** (`ADVICE` / `WARNING` / `ERROR`), - optionally a **location** (file/line, or a task path), - optionally **solutions** and a **documentation link**. ## Why structured Because one report fans out to **many consumers**: 1. The **console** prints a deduplicated summary. 2. Gradle writes an **HTML problems report** under the build dir. 3. **IDEs** receive problems over the **Tooling API** and render inline annotations. 4. **Build scans** aggregate and categorize them. ## How you get the service You cannot `new` it — Gradle injects it. In a plugin or task you use constructor injection with `@Inject`: ```kotlin abstract class MyPlugin @Inject constructor( private val problems: Problems ) : Plugin<Project> ``` Then `problems.getReporter().report(id) { spec -> ... }`. ## Report vs. throw - `reporter.report(id) { ... }` records the problem **without** failing the build — perfect for warnings and advice. - `reporter.throwing(exception, id) { ... }` records it **and** throws, so it both fails the build *and* shows up structured. The key mental model: **logging is for humans reading a terminal; the Problems API is for every tool that consumes the build.**

  • Does reporting a problem fail the build?
    No. `reporter.report(...)` only records it. To also fail, use `reporter.throwing(exception, id) { ... }`, which records the structured problem and throws.
  • Who consumes a reported problem besides the console?
    The generated HTML problems report, IDEs via the Tooling API (as inline annotations), and build scans, which can aggregate and categorize them.

Logging is a sticky note shouted into one room; a structured problem is a filed ticket with an ID and category that every department (IDE, console, scan) can read.

saying these in an interview costs you the question

  • Saying the Problems API replaces exceptions for control flow — it's about diagnostics, not flow.
  • Claiming report() fails the build.

context

open as a page

How do you obtain the Problems service inside a plugin or task, and why can't you just instantiate it?

level: middleimportance: must knowfreq 30%

basics

~10 s

You don't create it — Gradle injects it. Add a constructor parameter of type Problems annotated with @Inject on your Plugin or Task (using an abstract class), and Gradle's service registry supplies the instance.

open as a page

What are ProblemId and ProblemGroup, and why does giving a problem a stable identity matter?

level: middleimportance: must knowfreq 28%

basics

~20 s

A ProblemGroup is a namespace (e.g. your plugin's category) and a ProblemId is a stable identity within it — a name plus human displayName. The stable ID lets Gradle and IDEs group, count, and deduplicate the same kind of problem.

open as a page

When reporting a problem, what is the difference between contextualLabel and details, and how do they relate to the ProblemId's displayName?

level: middleimportance: should knowfreq 22%

basics

~20 s

The ProblemId displayName is the fixed category label. contextualLabel is the short, occurrence-specific headline (what's wrong here). details is longer explanatory text expanding on it. The label is the summary line; details is the body.

open as a page

Walk through reporting a non-fatal problem versus a fatal one with the Problems API. When do you use report() versus throwing()?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Use reporter.report(id) { ... } for non-fatal diagnostics (warnings/advice) — it records the structured problem but the build continues. Use reporter.throwing(exception, id) { ... } when the issue must fail the build — it records the same structured data and throws.

open as a page

Your organization has several plugins that emit dozens of logger.warn warnings. How would you adopt the Problems API across them, and what design choices keep the resulting diagnostics coherent?

level: principalimportance: nice to knowfreq 12%

basics

~20 s

Define a shared ProblemGroup taxonomy and stable ProblemId naming across plugins, inject Problems through a common base, migrate warnings incrementally (keeping ids stable), and keep occurrence-specific data in contextualLabel/details/location so consumers can group and aggregate consistently.

open as a page