What are common pitfalls when writing onlyIf predicates, particularly around side effects, exceptions, and lazy configuration?
answer
- pure boolean, no side effects
- throwing fails the build
- capture Providers for config cache
- keep predicate cheap
- not a caching mechanism
basics
~20 sKeep onlyIf predicates pure and cheap: no mutating state, no heavy I/O, and don't throw (an exception there fails the build, not a clean skip). Read inputs lazily via Providers, and don't rely on configuration order since the body runs at execution.
solid answer
~50 sPitfalls: (1) **Side effects** — the predicate may be evaluated and its body should not mutate task/project state or create files; treat it as a pure boolean. (2) **Throwing** — an exception inside `onlyIf` propagates and **fails the build**, it does not skip; guard risky lookups. (3) **Expensive work** — keep it cheap; it runs on the execution path and slow predicates inflate build time. (4) **Configuration-cache compatibility** — referencing the live `Project` at execution breaks the configuration cache; capture needed values via `Provider`/`Property` or task inputs at configuration time and read them in the predicate. (5) **Order assumptions** — the predicate runs at execution time, after configuration; don't assume some configuration block runs later. (6) **Caching confusion** — using `onlyIf` to express input-based skipping defeats incrementality; use declared inputs/outputs instead. A robust predicate reads captured providers, returns a boolean, throws nothing, and mutates nothing.
code
kotlin · 4 linesval enabled = providers.gradleProperty("enablePublish").isPresent
tasks.register("publish") {
onlyIf("-PenablePublish required") { enabled } // captured at config time, config-cache safe
}go deeper
Know the predicate should just return true/false and not do real work.
Understand that throwing fails the build and that predicates should be cheap and side-effect-free.
Make predicates configuration-cache-safe by capturing Providers and avoid using onlyIf for caching concerns.
Establish review rules for build logic: pure predicates, captured inputs, no live-project access, reasons attached; audit for config-cache regressions.
## Treat the predicate as a pure function An `onlyIf` spec should answer one question — run or not — and have **no side effects**. Don't create files, register tasks, or mutate properties inside it. Gradle evaluates it on the execution path, and side effects there are surprising and can break incremental builds and the configuration cache. ## Exceptions fail the build If the predicate throws, the exception propagates and the build **fails** — you do NOT get a clean SKIPPED. So defensive code matters: ```kotlin onlyIf { val f = layout.projectDirectory.file("flag.txt").asFile f.exists() && f.readText().trim() == "go" // guard before reading } ``` If reading might fail, wrap it or check existence first; don't let an IOException escape. ## Configuration cache The configuration cache serializes the task graph and forbids touching the live `Project`/`Task` script object at execution. A predicate that calls `project.hasProperty(...)` or `project.file(...)` at execution time can break the cache. The fix is to **capture values at configuration time** into a `Provider`/`Property` or task input and read those in the predicate: ```kotlin val shouldRun = providers.gradleProperty("enablePublish").map { true }.orElse(false) tasks.register("publish") { onlyIf { shouldRun.get() } // reads a captured provider, not live project } ``` ## Keep it cheap The predicate runs for every requested task; a slow network call or directory walk inside it adds latency to every build. Precompute when possible. ## Don't reach for onlyIf to do caching's job If the real condition is 'inputs unchanged → skip', declare `@InputFiles`/`@OutputFiles` (or `inputs`/`outputs`) and let the up-to-date check and build cache handle it. `onlyIf` is for orthogonal runtime gates, not incrementality. ## Summary checklist - Pure, side-effect-free, returns a boolean. - Never throws (guard risky reads). - Reads captured `Provider`/`Property`, not the live project (config-cache safe). - Cheap to evaluate. - Not a substitute for input/output declarations.
- Your onlyIf predicate calls project.file(...) at execution and the configuration cache complains. Fix?Capture the value at configuration time into a Provider/Property (e.g. providers.gradleProperty / layout) and read that inside the predicate instead of the live Project.
- What happens if the predicate throws an exception?The build fails — the exception propagates; it does not produce a SKIPPED outcome. Guard risky operations.
saying these in an interview costs you the question
- Putting file creation or state mutation inside onlyIf.
- Assuming a thrown exception in onlyIf just skips the task.
- Calling the live Project at execution under the configuration cache.
- Using onlyIf to emulate up-to-date/input-based skipping.