Explain exactly when a finalizer task runs and when it doesn't, including failure and skip cases.
answer
- runs iff finalized task reached execution
- fails -> finalizer still runs, build still fails
- up-to-date/from-cache still counts as executed
- prerequisite-failed -> A never runs -> finalizer skipped
- attach teardown to the resource-owning task
basics
~20 sA finalizer runs after its finalized task whenever that task actually starts executing — success or failure. If the finalized task never executes (e.g. a prerequisite failed, or it isn't in the graph), the finalizer does not run.
solid answer
~50 sThe finalizer's execution hinges on whether the **finalized** task *executed*: - **Finalized task succeeds** → finalizer runs. - **Finalized task runs and fails** → finalizer still runs (the headline guarantee), then the build fails. - **Finalized task is UP-TO-DATE / FROM-CACHE / NO-SOURCE** → it still counts as having 'executed' (it was scheduled and processed), so the finalizer runs. - **Finalized task never starts** because one of *its* `dependsOn` prerequisites failed first → the finalized task didn't execute, so the finalizer is **not** run. - **Finalized task isn't requested at all** (not in the graph) → finalizer isn't pulled in by the finalization relationship alone. So the precise rule is: 'finalizer runs iff the finalized task is scheduled and reaches execution.' This is why finalizers are safe for teardown of resources that a *preceding setup* task created — but you must make the setup task part of the finalized task's own graph carefully, or use a setup task that the finalizer can tolerate not having run.
code
kotlin · 4 linestasks.register("a") { doLast { throw GradleException("boom") } }
tasks.register("f") { doLast { println("cleanup ran") } }
tasks.named("a") { finalizedBy("f") }
// ./gradlew a -> "cleanup ran" prints, then BUILD FAILEDgo deeper
Know the headline: finalizer runs after, even on failure. The skip edge cases are bonus.
Enumerate the runs/doesn't-run cases, especially the prerequisite-failed-so-finalizer-skipped gotcha.
Reason about resource-ownership placement of teardown and partial-startup leaks.
Define team conventions for reliable teardown (shared build services, lifecycle ownership) to avoid these traps systemically.
## The precise rule A finalizer **F** of task **A** runs **iff A is scheduled in the build and A reaches the executing state.** Let's unpack every case, because interviewers probe exactly here. ### A executes and succeeds F runs after A. Normal case. ### A executes and fails F **still runs** — this is the defining feature. After F completes, Gradle reports the build as failed because A failed. F running does not change the outcome. ```kotlin tasks.register("a") { doLast { throw GradleException("boom") } } tasks.register("f") { doLast { println("cleanup ran") } } tasks.named("a") { finalizedBy("f") } // ./gradlew a -> prints "cleanup ran", then BUILD FAILED ``` ### A is UP-TO-DATE, FROM-CACHE, NO-SOURCE, or SKIPPED-but-processed These still count as A having been scheduled and resolved. F runs. (Incremental/cache outcomes are normal task states, not 'A never executed'.) ### A never starts because A's own dependency failed If A `dependsOn(B)` and B fails, A never executes — and therefore **F does not run.** This surprises people: 'my cleanup didn't run!' The reason is there was nothing to finalize; A never touched any resource. If your setup is in B and you need teardown regardless, restructure: make the setup itself `finalizedBy` the teardown, or keep setup inside A. ### A is not in the requested graph Finalization is forward-only. Running F directly does not pull A in. And if A is never requested, F is not pulled in by the finalization edge. ## A worked example of the dependency-failed gotcha ```kotlin tasks.register("startServer") tasks.register("stopServer") tasks.register("integTest") { dependsOn("startServer") finalizedBy("stopServer") } ``` If `startServer` itself fails, `integTest` never runs, so `stopServer` does **not** run. That's usually fine (the server never came up). But if `startServer` *partially* started the server before failing, you have a leak. The robust pattern is to attach the finalizer to the task that actually owns the resource lifecycle, or use a built-in test-fixture/`TestServer` service. ## Multiple finalizers and chains A task may have several finalizers; all are scheduled. A finalizer can itself have finalizers, forming a chain. Each link follows the same 'finalized task executed?' rule. ## Parallel execution note With `--parallel`, Gradle still respects that finalizers run after their finalized task; they don't start until the finalized task is done, even though unrelated tasks may run concurrently. ## Practical guidance - Put teardown on the task that *creates* the resource so the 'A executed' guarantee lines up with 'resource exists'. - Don't rely on finalizers to clean up after a *prerequisite* that may fail before A runs. - Remember the build outcome reflects A's failure even though cleanup ran.
- My finalizer didn't run when a dependsOn prerequisite of the finalized task failed. Why?Because the finalized task never executed — its prerequisite failed first. Finalizers only run when the finalized task reaches execution. Attach teardown to the task that actually owns the resource instead.
- If the finalized task is UP-TO-DATE, does the finalizer run?Yes. UP-TO-DATE (and FROM-CACHE/NO-SOURCE) still counts as the task being scheduled and processed, so the finalizer runs.
saying these in an interview costs you the question
- Claiming the finalizer always runs even if the finalized task's prerequisite failed first — it doesn't.
- Thinking UP-TO-DATE means the finalizer is skipped.
- Believing a successful finalizer turns a failed build green.