skip to content

What are the behavioral differences between tasks.create and tasks.register with respect to the configuration phase, and what subtle bugs can surface when migrating?

level: seniorimportance: should knowfreq 40%

answer

  1. create = eager, register = lazy provider
  2. config block may never run if unrealized
  3. side effects in config block break
  4. getByName after register re-realizes
  5. all{} → configureEach{}

basics

~20 s

create configures the task immediately during configuration; register defers it until realized. Migrating can surface ordering bugs: code that relied on a task existing/being configured eagerly (e.g. getByName right after) may now find it unrealized.

solid answer

~50 s

`tasks.create` both creates and runs the configuration action eagerly during the configuration phase, so the task and its effects exist immediately. `tasks.register` returns a `TaskProvider` and runs the configuration action only when the task is realized. The behavioral gap that bites during migration: any code that assumed the task was already created/configured — e.g. an immediate `tasks.getByName(name)`, side effects in the config block expected to run on every build, or another plugin reacting via `tasks.all { }` — may now observe the task later or never. Two subtle classes: (1) **config blocks with side effects** (mutating shared state, registering extensions) that silently stop running when the task isn't realized; (2) **ordering assumptions** where the registration block reads another task/property that isn't ready yet. Fixes: move cross-task wiring to provider-based connections, use `configureEach` for reactive configuration, and never put build-wide side effects inside a task's configuration block — keep it about configuring that task only.

code

kotlin · 7 lines
kotlin
// before (eager) — config always runs, getByName harmless
tasks.create("audit") { doLast { runAudit() } }
val a = tasks.getByName("audit")

// after (lazy) — keep it lazy end-to-end
val audit = tasks.register("audit") { doLast { runAudit() } }
tasks.named("audit") { group = "verification" }   // not getByName

go deeper

for a junior

Know create is eager and register is lazy and returns a provider.

for a middle

Explain that a registered task's config runs only when realized and to use named over getByName.

for a senior

Anticipate side-effect and ordering bugs during migration and apply the provider-based fixes.

for a principal

Define migration playbooks and review rules so large codebases adopt registration without behavioral regressions.

## create vs register - **`tasks.create(name) { config }`** — *eager*: the `Task` is instantiated and `config` runs **immediately** during the configuration phase. Returns the concrete `Task`. - **`tasks.register(name) { config }`** — *lazy*: returns a `TaskProvider<T>`; `config` runs only when the task is **realized** (part of the requested graph, or forced). Functionally the configured task is identical once realized; the difference is *when* (and *whether*) the configuration action runs. ## Why migration can surface bugs Eager code often relied — sometimes accidentally — on the configuration block running every build. With registration, if the task isn't realized, that block never executes. Watch for: ### 1. Side effects in the configuration block ```kotlin // BAD: side effect that other code depends on tasks.register("report") { project.extensions.extraProperties["reportReady"] = true // runs only if realized! } ``` The configuration block should configure *that task* and nothing else. Build-wide state set here becomes nondeterministic under avoidance. ### 2. Eager lookups right after registration ```kotlin val p = tasks.register("x") { /* ... */ } val t = tasks.getByName("x") // forces realization NOW — defeats the point ``` If old code did `create` then `getByName`, the `getByName` was harmless; after switching to `register` it re-introduces eager realization. Replace with `tasks.named("x")`. ### 3. Reactive configuration via all {} A plugin using `tasks.all { if (name == "x") ... }` realizes everything. Under registration you want `tasks.configureEach { }` or `tasks.named("x") { }`, which react lazily. ### 4. Ordering / readiness A registration block that reads another task's not-yet-set property can see a default/empty value if it runs at a different time than before. Use lazy providers (`map`/`flatMap`) so the value is read when queried, not when the block runs. ## Safe migration recipe 1. Swap `create`→`register`, `getByName`→`named`, `all{}`→`configureEach{}`, eager `withType{}`→`withType().configureEach{}`. 2. Move any cross-cutting side effects out of task config blocks (into plugin `apply`, `pluginManager.withPlugin`, or provider wiring). 3. Replace `.get()` value reads with `map`/`flatMap`. 4. Re-run the build (and the Configuration Cache if enabled) to surface tasks that silently stopped configuring. ## A note on `maybeCreate` / `registerIfAbsent` For idempotent registration, prefer `registerIfAbsent` (e.g. for build services or container elements) over eager `maybeCreate`, keeping the defer-until-needed property intact.

  • Why is putting build-wide side effects in a task's configuration block dangerous under configuration avoidance?
    The block runs only if the task is realized. If the task isn't part of the requested graph, the side effect silently never happens, making builds nondeterministic. Configuration blocks should configure only their own task.
  • After migrating to register, a plugin's reaction stopped firing. Likely cause?
    It probably relied on tasks.all {} or an eager getByName that used to realize the task. Switch the reaction to tasks.configureEach {} or named(), and wire by provider so it reacts lazily.

saying these in an interview costs you the question

  • Putting cross-cutting side effects inside a task configuration block.
  • Re-adding eager realization via getByName immediately after register.
  • Assuming register and create are drop-in identical with no timing differences.

context