Walk through reporting a non-fatal problem versus a fatal one with the Problems API. When do you use report() versus throwing()?
answer
- report = record + continue
- throwing = record + fail (returns the exception to throw)
- throwing beats bare throw: structured failure
- WARNING/ADVICE -> report; ERROR -> throwing
- collect reports, then one throwing/aggregate
basics
~20 sUse 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.
solid answer
~50 sThe `ProblemReporter` offers two entry points with the same builder. **`report(id) { spec -> ... }`** records a structured problem and **returns** — the build keeps running. Use it for warnings, advice, deprecations, lint findings: things the user should see but that don't invalidate the build. **`throwing(exception, id) { spec -> ... }`** records the structured problem **and throws** the supplied exception, so the build fails *and* the failure carries machine-readable identity, contextualLabel, severity, and location. Use it for genuine errors: invalid configuration, contract violations. The win over a bare `throw GradleException(...)` is that the failure is now structured — an IDE can annotate it inline and a scan can categorize it, instead of getting only a stack trace. Pattern: collect many `report` calls during a check, then if any are fatal, finish with a single `throwing` (or throw an aggregate). Don't `throwing` for advisory issues, and don't silently `report` something that should actually stop the build.
code
kotlin · 13 linesval group = ProblemGroup.create("schema", "Schema validation")
val warnId = ProblemId.create("nullable-fk", "Nullable foreign key", group)
val errId = ProblemId.create("missing-pk", "Missing primary key", group)
// non-fatal: record and keep going
problems.reporter.report(warnId) { it.contextualLabel("FK 'owner_id' is nullable").severity(Severity.WARNING) }
// fatal: record AND throw
if (table.primaryKey == null) {
throw problems.reporter.throwing(GradleException("Invalid schema"), errId) {
it.contextualLabel("Table '${table.name}' has no primary key").severity(Severity.ERROR)
}
}go deeper
Know report() doesn't fail and throwing() does.
Map severities to the right call and explain the return-the-exception idiom of throwing().
Design the collect-then-fail flow and justify throwing() over bare throw for IDE/scan structure.
Set org policy for what severity warrants failure and ensure plugins fail builds consistently with structured problems.
## Two ways to emit, one builder Both methods take the same `ProblemId` and the same spec-builder lambda; they differ only in **build outcome**. ### `report(id) { ... }` — record and continue ```kotlin problems.reporter.report(id) { spec -> spec.contextualLabel("Deprecated config 'foo' used") .severity(Severity.WARNING) } // execution continues normally ``` The problem is recorded and routed to all consumers, but control returns to your code. This is the right choice for **warnings, advice, deprecations, lint findings** — anything the user should be told about but which shouldn't abort the build. ### `throwing(exception, id) { ... }` — record and fail ```kotlin throw problems.reporter.throwing( GradleException("Invalid schema"), id ) { spec -> spec.contextualLabel("Table 'orders' missing primary key") .severity(Severity.ERROR) } ``` `throwing` attaches the structured problem to the exception and throws, so the **build fails** while the failure carries identity, label, severity, and location. The signature returns the exception, so you typically `throw problems.reporter.throwing(...)`. ## Why throwing beats a bare throw `throw GradleException("Invalid schema")` aborts the build but gives consumers only a message + stack trace. With `throwing`, the **same** failure is structured: the IDE can render it inline on the offending line; a build scan can bucket it under your `ProblemId`; a dashboard can trend it. You lose nothing and gain machine-readability. ## Choosing between them | Situation | Use | |---|---| | Deprecation, style/lint warning, advice | `report` (WARNING/ADVICE) | | Invalid config that must stop the build | `throwing` (ERROR) | | Many findings, some fatal | many `report`, then one `throwing` / aggregate | ## Anti-patterns - Using `throwing` for advisory issues — annoys users and fails good builds. - Using `report` for something that should fail — the build proceeds with bad state. - Throwing a raw exception when you already have a `ProblemId` — you forfeit the structured failure for free.
- Why prefer throwing() over a plain throw GradleException?Both fail the build, but throwing() attaches a structured problem (id, label, severity, location) to the failure so IDEs can annotate it inline and scans can categorize it. A bare throw gives only a stack trace.
- How do you report many findings but fail only if any are fatal?Call report() for each finding during the check, track whether any reached ERROR, and at the end issue a single throwing() (or throw an aggregate exception) if a fatal one occurred.
- Does report() guarantee the build succeeds?report() itself never fails the build, but other code or a later throwing() still can. report() simply records without throwing.
saying these in an interview costs you the question
- Saying report() can fail the build by itself.
- Using throwing() for advisory warnings.
- Throwing a raw exception when a ProblemId is available, forfeiting structured failure.