skip to content

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%

answer

  1. report = record + continue
  2. throwing = record + fail (returns the exception to throw)
  3. throwing beats bare throw: structured failure
  4. WARNING/ADVICE -> report; ERROR -> throwing
  5. collect reports, then one throwing/aggregate

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.

solid answer

~50 s

The `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 lines
kotlin
val 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

for a junior

Know report() doesn't fail and throwing() does.

for a middle

Map severities to the right call and explain the return-the-exception idiom of throwing().

for a senior

Design the collect-then-fail flow and justify throwing() over bare throw for IDE/scan structure.

for a principal

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.

context