What is `finalizedBy`, what are its key semantics (especially on failure), and what is a canonical use case?
answer
- finalizer runs after, even on failure
- ideal for teardown/cleanup
- only runs if finalized task runs
- start fixture -> finalizedBy stop
- not a 'before' dependency
basics
~20 sa.finalizedBy(b) schedules b to run after a, even if a fails. It's used for guaranteed cleanup or teardown — like stopping a server or collecting reports — that must happen whether or not the main task succeeded.
solid answer
~40 s`taskA.finalizedBy(taskB)` declares `taskB` as a **finalizer** of `taskA`. Whenever `taskA` is scheduled and starts executing, Gradle also schedules `taskB` to run **after** it — and crucially, the finalizer runs **even if `taskA` fails or throws**. This is the distinguishing property: unlike `dependsOn`/`mustRunAfter`, finalizers are about guaranteed teardown. Semantics worth knowing: the finalizer only runs if its finalized task actually *executed* (was not skipped/up-to-date in a way that prevented it from being scheduled — practically, if the finalized task runs, the finalizer runs). The finalizer is pulled into the graph by the relationship. The canonical use case is resource lifecycle: start a fixture (e.g. a test server or Docker container) for an integration test and `finalizedBy` a `stop`/`teardown` task so the resource is released even when the test fails — avoiding leaked processes.
code
kotlin · 6 linesval startDb = tasks.register("startTestDb")
val stopDb = tasks.register("stopTestDb")
tasks.register<Test>("integrationTest") {
dependsOn(startDb)
finalizedBy(stopDb) // stopTestDb runs after, even if the tests fail
}go deeper
Know finalizedBy runs a cleanup task after another, even on failure.
Give the start-fixture / stop-fixture pattern and state the run-even-on-failure guarantee plus the 'only if finalized task ran' nuance.
Contrast with doLast and mustRunAfter, discuss --continue interaction and resource-leak avoidance.
Standardize teardown across a build's integration suites so external resources are never leaked in CI regardless of failures.
## What finalizedBy means ```kotlin val startServer = tasks.register("startServer") val stopServer = tasks.register("stopServer") val integrationTest = tasks.register<Test>("integrationTest") { dependsOn(startServer) finalizedBy(stopServer) // stopServer runs after integrationTest, even on failure } ``` `finalizedBy` registers a **finalizer**. When the *finalized* task (`integrationTest`) is part of the build and runs, its finalizer (`stopServer`) is scheduled to run afterward. ## The failure guarantee The defining behavior: **if the finalized task fails, the finalizer still runs.** That's why it's the right tool for cleanup/teardown. With plain `mustRunAfter`, an exception in the main task would normally abort the build before the ordering target ran (depending on `--continue`); `finalizedBy` is specifically designed to run the finalizer regardless of outcome. ## Important nuances - **Only runs if the finalized task runs.** If the finalized task is not scheduled at all (you never requested it / it was filtered out), its finalizer is not pulled in. In current Gradle, if the finalized task is up-to-date or skipped, the finalizer is generally not executed because there's nothing to finalize. - **Finalizers can have their own dependencies**, which are scheduled as needed when the finalizer runs. - **`--continue` interaction:** finalizers help ensure cleanup happens even amid failures; combined with `--continue` Gradle still runs finalizers for the tasks that executed. - It is NOT a dependency in the 'must produce output first' sense — it's strictly an *after, including on failure* relationship. ## Canonical use cases 1. **Tear down test fixtures**: stop an embedded server, kill a spawned process, remove a temp DB. 2. **Always-collect diagnostics**: gather logs / generate a report after a long task whether or not it passed. 3. **Release locks / external resources** acquired by a task. ## Contrast table (mental model) - `dependsOn` → run B **before** A; B selected. - `mustRunAfter` → if both run, B **after** A; not selected; not failure-resilient by design. - `finalizedBy` → B **after** A, **even if A fails**; B is the cleanup. ```kotlin tasks.register("withCleanup") { doLast { throw GradleException("boom") } finalizedBy("cleanup") // cleanup STILL runs despite the exception } ```
- If `integrationTest` fails with an exception, does `stopTestDb` still run?Yes — that's the whole point of `finalizedBy`: the finalizer runs even when the finalized task fails.
- If `integrationTest` is never scheduled, does its finalizer run?No. The finalizer is only pulled in and run because its finalized task runs; with no finalized task there is nothing to finalize.
- How does this differ from putting cleanup in a `doLast` block?A `doLast` in the same task wouldn't run if an earlier action threw; a finalizer is a separate task guaranteed to run after, even on failure.
Like a try { ... } finally { cleanup() } block, but for tasks: the finally (finalizer) runs whether the body succeeded or threw.
saying these in an interview costs you the question
- Saying the finalizer runs before the finalized task (it runs after).
- Claiming the finalizer is skipped when the main task fails (it specifically runs anyway).
- Confusing finalizedBy with dependsOn semantics.