skip to content

Using providers.gradleProperty, how do you supply a default and handle a missing property without breaking the configuration cache?

level: middleimportance: should knowfreq 40%

answer

  1. absent != empty string
  2. .orElse for lazy default
  3. .getOrNull / .getOrElse for eager
  4. never blind .get() on optional prop
  5. wire so cache tracks the input

basics

~10 s

Use .orElse("default") or .getOrElse("default") on the provider, or .getOrNull() to detect absence. Avoid calling .get() at configuration time when the property might be missing.

solid answer

~40 s

`providers.gradleProperty("x")` returns an *absent* provider when `x` is undefined. To stay lazy and config-cache-safe you keep working with the provider: - `.orElse("default")` returns a new lazy provider that yields the default when absent — best for wiring into task inputs. - `.map { it.toBoolean() }` transforms lazily; combine with `.orElse(false)`. - `.isPresent` reports presence without forcing a value (when wired). If you must read eagerly (rare), `getOrElse("default")` and `getOrNull()` resolve immediately. The key discipline is **not** calling `.get()` on a possibly-absent provider at configuration time — that throws *and* re-introduces an eager read. Prefer pushing the default into the provider chain and letting Gradle resolve it at execution, so the value is tracked as a cache input and missing-property handling is uniform.

code

kotlin · 8 lines
kotlin
// Lazy default, config-cache-safe
val mode = providers.gradleProperty("mode").orElse("release")

// Lazy transform + default
val verbose = providers.gradleProperty("verbose").map { it.toBoolean() }.orElse(false)

// Guarded eager (only when you must decide at config time)
if (verbose.get()) logger.lifecycle("verbose on")

go deeper

for a junior

Know .orElse("default") supplies a fallback and .getOrNull() detects a missing property.

for a middle

Explain the lazy vs eager handlers and why blind .get() on optional props is wrong.

for a senior

Discuss how wiring with .orElse keeps the value as a tracked cache input and chain across sources.

for a principal

Set conventions for optional-property handling and defaults so behavior is consistent and cache-correct across modules.

## Absence is a first-class state A `Provider<String>` from `providers.gradleProperty("x")` is either *present* (the property exists) or *absent* (it doesn't). This is different from an empty string — absence means "no value at all." How you handle absence determines both correctness and config-cache safety. ## Lazy handlers (preferred) Keep operating on the provider; resolution happens later: ```kotlin // supply a default lazily val mode = providers.gradleProperty("mode").orElse("release") // transform lazily, then default val verbose = providers.gradleProperty("verbose") .map { it.toBoolean() } .orElse(false) tasks.register("info") { val m = mode // capture provider, not value val v = verbose doLast { println("mode=${m.get()} verbose=${v.get()}") } // .get() at execution time is fine } ``` `orElse` accepts a literal *or another provider*, so you can chain across sources. `.map`/`.flatMap` transform without forcing the value. None of these capture a `Project`, so the configuration cache is happy. ## Eager handlers (use sparingly) Sometimes you genuinely need a value during configuration (e.g., to decide whether to register a task). Then: - `getOrElse("default")` — resolves now, returns default if absent. - `getOrNull()` — resolves now, returns null if absent. - `get()` — resolves now, **throws** `MissingValueException` if absent. These are still config-cache-safe in the sense that they don't touch `Project`; the risk is only that you've moved work to configuration time and lose laziness. A guarded pattern: ```kotlin if (providers.gradleProperty("enableX").map { it.toBoolean() }.getOrElse(false)) { tasks.register("x") { /* ... */ } } ``` ## The anti-pattern Calling `.get()` on a maybe-absent provider at configuration time: ```kotlin val x = providers.gradleProperty("x").get() // throws when absent; eager ``` This both crashes for users who didn't set `x` and forces an eager read. Replace with `.orElse(...)` (wire lazily) or `.getOrElse(...)` (guarded eager). ## Why config-cache cares When the provider (with its `orElse` default) is wired into a task input, Gradle records the property as an input. If the user later defines `x`, the recorded input changes and the cache invalidates correctly. An eager `.get()` value stored in a local variable would *not* be re-evaluated on a cache hit unless wired, leading to stale results — another reason to prefer lazy handling.

  • What does .get() do when the property is absent?
    It throws a MissingValueException. Use .getOrNull(), .getOrElse(default), or wire .orElse(default) to avoid forcing an absent value.
  • Why prefer .orElse over getOrElse when wiring into a task input?
    .orElse keeps the whole chain lazy and lets Gradle track the property as a cache input that re-evaluates on changes; getOrElse resolves eagerly at configuration time and is not re-evaluated.

saying these in an interview costs you the question

  • Calling .get() unconditionally on an optional property and assuming a default — it throws.
  • Confusing absence with an empty string value.

context