Gradle Best Practices & Troubleshooting
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Gradle 9.8.1 (released on 7 October 2026), with Gradle 8.14.6, the last 8.x release, mentioned where it differs. Every example marked as run was executed with the
9.8.1 wrapper on 2026-10-16, on macOS (Apple silicon), in scratch copies of the tidepool build (a root project with the subprojects tidepool-model, tidepool-engine and tidepool-cli, a version catalog, and
repositories declared in settings.gradle.kts), once with *.gradle.kts files and once with *.gradle files. The Gradle daemon ran on Oracle JDK 26 (java version "26"), the default JDK of the machine, and the code
compiles with --release 21. The gradle.properties of the examples sets org.gradle.jvmargs=-Xmx512m -Dfile.encoding=UTF-8 -Duser.language=en -Duser.country=US, so that messages come out in English instead of the machine’s
Spanish, and org.gradle.configuration-cache=true: the examples are compatible with the configuration cache (a second run reused the entry) except the one that fails on purpose, which is said where it happens. Runs that
need to show a problem the cache would hide use --no-configuration-cache, and say so. The normal runs used the shared Gradle User Home; only the debugger run used a Gradle User Home created for it. The scratch directory is shown as <work>.
Gradle 8.14.x was not available here, so what is said about it comes from the official release notes and upgrade guides and is labelled "not run here". Also not run here: publishing a Build Scan,
a daemon that cannot connect because of network address translation, refreshing an IDE, attaching a real debugger, and the wrapper validation action for GitHub Actions.
This is a synthesis page: it collects the advice that Gradle publishes as its best practices and its troubleshooting guide, points to the page of this site that explains each topic in depth instead of repeating it, and backs the advice that can be checked with a real run. The first half is about how to write a build that is easy to keep healthy; the second half is about what to do when it is not. For the other two tools, the equivalents are Maven Troubleshooting & Best Practices and, for Ant, the diagnostics options in Running Ant; a table near the end compares them. The cross-tool background is in Build Automation Fundamentals and in Comparing Ant, Maven and Gradle.
The official best-practices index
Gradle keeps a list of recommendations, the Best Practices for Gradle, split into pages by theme (general, structuring builds, dependencies, tasks, performance, security and testing). Each practice has an explanation, a "Don’t do this" and a "Do this instead" example (in both DSLs), references and tags, and the index records the Gradle version in which the practice was added to the list. That column is not the version in which a feature appeared; it tells you which advice is new to you if you last read the list some releases ago. The oldest entries are marked 8.14; newer entries are added in nearly every minor release (the latest at the time of writing is marked 9.8.0), so rereading the index after each upgrade is cheap and worth doing. The index counts 47 practices on 2026-10-16.
The tables below reproduce the index (title, section and release) and add the page of this site that covers the topic. The practices are advice, not rules: several of them (for instance "Avoid afterEvaluate") have
narrow, documented exceptions, which the official page lists.
| Practice | Added in | Detailed page |
|---|---|---|
Use Kotlin DSL |
8.14 |
|
Use the Latest Minor Version of Gradle |
8.14 |
|
Apply Plugins Using the |
8.14 |
|
Do Not Use Internal APIs |
8.14 |
|
Set build flags in |
9.0.0 |
|
Name Your Root Project |
9.2.0 |
|
Do not use |
9.2.0 |
|
Avoid |
9.6.0 |
|
Consider use of |
9.7.0 |
|
Obtain Loggers via |
9.8.0 |
| Practice | Added in | Detailed page |
|---|---|---|
Modularize Your Builds |
9.0.0 |
|
Do Not Put Source Files in the Root Project |
9.0.0 |
|
Favor |
9.0.0 |
|
Avoid Unintentionally Creating Empty Projects |
9.1.0 |
|
Use Convention Plugins |
9.3.0 |
| Practice | Added in | Detailed page |
|---|---|---|
Single GAV String |
8.14 |
|
Use Version Catalogs to Centralize Dependency Versions |
9.0.0 |
|
Name Version Catalog Entries Appropriately |
9.0.0 |
|
Set up your Dependency Repositories in the Settings file |
9.0.0 |
|
Don’t Explicitly Depend on the Kotlin Standard Library |
9.0.0 |
|
Avoid Redundant Dependency Declarations |
9.0.0 |
|
Use Content Filtering with multiple Repositories |
9.1.0 |
|
Apply Exclusions Narrowly |
9.2.0 |
|
Always Declare Attributes on Consumable and Resolvable Configurations |
9.7.0 |
| Practice | Added in | Detailed page |
|---|---|---|
Avoid |
8.14 |
|
Favor |
8.14 |
|
Group and Describe custom Tasks |
9.0.0 |
|
Do not call |
9.1.0 |
|
Don’t resolve Configurations before Task Execution |
9.1.0 |
|
Avoid using eager APIs on File Collections |
9.1.0 |
|
Prefer |
9.2.0 |
|
Use unique output files and directories |
9.3.0 |
|
Don’t hardcode task names when referring to them |
9.7.0 |
|
Don’t access a Project instance during Task Execution |
9.7.0 |
|
Wiring Task Outputs with |
9.7.0 |
|
Favor collection property types over a Property holding a collection |
9.8.0 |
| Practice | Added in | Detailed page |
|---|---|---|
Enable UTF-8 |
9.0.0 |
|
Use the Build Cache |
9.1.0 |
|
Use the Configuration Cache |
9.1.0 |
|
Avoid Expensive Computations in Configuration Phase |
9.0.0 |
|
Prefer the |
9.4.0 |
| Practice | Added in | Detailed page |
|---|---|---|
Validate the Gradle Distribution SHA-256 Checksum |
9.1.0 |
|
Validate the Gradle Wrapper on every Upgrade |
9.3.0 |
|
Do not Run |
9.7.0 |
|
Build Output Should Be Byte-for-Byte Reproducible |
9.7.0 |
|
Build your Published Artifacts Securely |
9.8.0 |
| Practice | Added in | Detailed page |
|---|---|---|
Test your custom Task and Plugins with TestKit |
9.4.0 |
Source: Gradle: Best Practices for Gradle, index, and the pages best_practices_general, best_practices_structuring_builds, best_practices_dependencies, best_practices_tasks, best_practices_performance, best_practices_security and best_practices_testing under the Gradle User Manual
Reading the list as a few themes
Forty-seven items are easier to remember as six habits.
-
Say what you mean in one place. Name the root project, declare repositories once in
settings.gradle(.kts), keep versions in a catalog, put build flags in the rootgradle.properties, and use theplugins {}block. Everything that is declared twice drifts apart. The pages are Settings & Build Scripts, Repositories & Content Filtering and Version Catalogs. -
Share logic through plugins, not through injection. A
build-logicincluded build with convention plugins replacesallprojects {}andsubprojects {}blocks andbuildSrc; see Convention Plugins & Sharing Build Logic. -
Be lazy. Register tasks instead of creating them, wire properties and providers instead of reading values early, and do not use
afterEvaluate; see Lazy Configuration & Providers. -
Declare what a task reads and writes. Inputs, outputs, path sensitivity and unique output directories are what make a task cacheable, up to date and safe to run in parallel; see Incremental Build and Build Cache.
-
Do not touch the live project at execution time. No
Project, no resolved configurations, noget()on a provider outside an action. This is what makes a build compatible with the configuration cache. -
Trust nothing you did not verify. Set
distributionSha256Sum, validate the wrapper after each upgrade, do not run./gradlewon a project you have not read, and make outputs reproducible; the cross-tool view is in Supply-Chain Security.
A starter configuration
The tidepool build used on this page follows the structural practices, which is why it is a good base for the experiments below. This is its settings.gradle.kts and gradle.properties.
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
org.gradle.jvmargs=-Xmx512m -Dfile.encoding=UTF-8 -Duser.language=en -Duser.country=US
org.gradle.configuration-cache=true
group=io.tidepool
version=1.0.0
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
What each line answers in the list of practices:
-
rootProject.nameis set, so task paths and reports do not depend on the name of the directory (or of the CI workspace) the build runs in. The practice was added to the list in 9.2.0. -
Repositories are declared once, in the settings file, with
FAIL_ON_PROJECT_REPOSso that a project cannot add its own; this works with the content filtering practice of Repositories & Content Filtering. -
Build flags live in the root
gradle.properties, which is checked into version control, instead of in command-line options that people forget. The official page also says not to restate a property at its default value: a default can change in a later release through the normal deprecation cycle, and then a build that never set it follows the change while one that restated it does not. This is whyorg.gradle.configuration-cache=trueis there (the configuration cache is recommended but still off by default in 9.8.1; the default is announced to change in Gradle 10) and whyorg.gradle.cachingis not set tofalse. -
-Dfile.encoding=UTF-8follows the performance practice that asks for UTF-8, because a platform-dependent default encoding changes the inputs of tasks between machines and causes cache misses. The locale options are only there to keep this page in English; a real build does not need them. -
The wrapper properties carry
distributionSha256Sumand the-bindistribution, as the security and performance practices ask. The page Gradle Wrapper explains every key; the verification is shown below. -
There is no
gradle.propertiesin any subproject. The official page says that support for subproject property files is inconsistent (Gradle itself, the Android Gradle plugin and the Kotlin Gradle plugin do not handle it reliably) and that a value for one subproject belongs in its build script, and a value for several subprojects in a convention plugin.
Practices you can check by running them
Four of the practices are claims that a few lines of build script and a command can confirm. Each of the examples below was added to a scratch copy of the tidepool build and run; the sources are the root build.gradle(.kts) and
tidepool-model/build.gradle(.kts) of that copy.
Wire lazily instead of using afterEvaluate
The practice (added to the list in 9.6.0) says: do not use project.afterEvaluate {} to configure tasks, wire properties or react to plugin application; use lazy properties and pluginManager.withPlugin().
The official reasons are that the order of several afterEvaluate callbacks is the order of registration, so a callback can see stale configuration; that tasks configured inside it are touched eagerly even if they never run;
and that it is incompatible with the configuration cache. It lists two narrow cases where it is still acceptable, fail-fast validation and reporting, and asks you to file a bug if the lazy APIs do not cover your case.
The reason people reach for afterEvaluate is that a value is set after the task that reads it has been defined. A Property solves that without a callback: the task holds the property, the value is read when the
task runs, and it does not matter which line sets it.
val bannerText = objects.property<String>()
tasks.register("bannerLazy") {
val text = bannerText
doLast { println(text.get()) }
}
bannerText.set("set after the task was wired")
def bannerText = objects.property(String)
tasks.register('bannerLazy') {
def text = bannerText
doLast { println text.get() }
}
bannerText.set('set after the task was wired')
To react to another plugin without afterEvaluate, use pluginManager.withPlugin("java") { ... }, which runs immediately if the plugin is already applied and as soon as it is applied otherwise (the details are in
Build Lifecycle and Lazy Configuration & Providers).
A pitfall of the Groovy version: a plain bannerText = 'text' on a local def variable replaces the variable with a String (the build then fails with No signature of method: java.lang.String.get(), which is what the first attempt
of this example did); a Property held in a local variable has to be set with set(). Task properties, in contrast, accept = in both DSLs.
The run (Kotlin DSL; the second run of the build, so the configuration cache entry was reused, which shows that the example is compatible with it; Groovy printed the same lines):
$ ./gradlew showVersionGood bannerLazy
Reusing configuration cache.
> Task :showVersionGood
version = 1.0.0
> Task :bannerLazy
set after the task was wired
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
Configuration cache entry reused.
Source: Gradle: Avoid afterEvaluate
Do not use the Project at execution time
The practice "Don’t access a Project instance during Task Execution" (9.7.0) is the most common reason for a build to be rejected by the configuration cache. The project is a live object of the configuration phase, and a cached
build does not have it. Here is the mistake, in a task that prints the project version in doLast, and the fix, which copies the value into a local variable while the task is being configured:
// Configuration cache trap: Project used while the task runs.
tasks.register("showVersionBad") {
doLast { println("version = ${project.version}") }
}
// The fix: capture the value while the task is configured.
tasks.register("showVersionGood") {
val version = project.version.toString()
doLast { println("version = $version") }
}
// Configuration cache trap: Project used while the task runs.
tasks.register('showVersionBad') {
doLast { println "version = ${project.version}" }
}
// The fix: capture the value while the task is configured.
tasks.register('showVersionGood') {
def version = project.version.toString()
doLast { println "version = ${version}" }
}
With the configuration cache on, the first one fails (the second one is the run shown in the previous section):
$ ./gradlew showVersionBad
Calculating task graph as no cached configuration is available for tasks: showVersionBad
> Task :showVersionBad FAILED
1 problem was found storing the configuration cache.
- Task `:showVersionBad` of type `org.gradle.api.DefaultTask`: invocation of 'Task.project' at execution time is unsupported with the configuration cache.
See https://docs.gradle.org/9.8.1/userguide/configuration_cache_requirements.html#config_cache:requirements:use_project_during_execution
See the complete report at file://<work>/tidepool/build/reports/configuration-cache/6dc51gbm7he8rkshqkjyjihbj/efs7x0f6zwvrp07qhehjbow2n/configuration-cache-report.html
[Incubating] Problems report is available at: file://<work>/tidepool/build/reports/problems/problems-report.html
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':showVersionBad' (registered in build file 'build.gradle.kts').
> Invocation of 'Task.project' by task ':showVersionBad' at execution time is unsupported with the configuration cache.
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.
BUILD FAILED in 1s
1 actionable task: 1 executed
Configuration cache entry discarded with 1 problem.
The Groovy run fails the same way. The differences are that the problem line names the build file and the line (Build file 'build.gradle': line 37: invocation of 'Task.project' ...), that the failure has a Where: section with Build file '<work>/tidepool/build.gradle' line: 37,
and that the registration is reported as (registered in build file 'build.gradle'). That last parenthesis is the task provenance added to error messages in Gradle 9.5.0; the 8.14 output has no such text (not run here, from the release notes).
The error message and the link to the explanation are the same in 8.14 (the configuration cache has been stable since 8.1). In a build that has just been taken to 9.x, --no-configuration-cache makes the failure disappear without fixing it,
so the right reaction is to read the report whose location is printed, not to switch the cache off. The cache itself is the subject of
Configuration Cache & Isolated Projects.
Reproducible archives
"Build output should be byte-for-byte reproducible" (9.7.0) is a security and a performance practice at once: if the same source produces the same bytes, anybody can check a binary against the source, and the build cache and the up-to-date checks work on stable inputs. The two usual causes of difference in a JAR are the timestamps of the entries and their order. Since Gradle 9.0.0 archive tasks are reproducible by default: timestamps are not preserved and the order of the entries is sorted. In Gradle 8.14 they were not (the 9.0.0 release notes list it as a change of default; not run here).
To see the effect, build the JAR of tidepool-model twice, two seconds apart, with a clean before each build, and compare the first 16 hex digits of the SHA-256 sum. Then do it with a switch that restores the old behaviour for archive tasks.
// Opt out of the reproducible-archive defaults to see the difference.
if (providers.gradleProperty("nonRepro").isPresent) {
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = true
isReproducibleFileOrder = false
}
}
// Opt out of the reproducible-archive defaults to see the difference.
if (providers.gradleProperty('nonRepro').present) {
tasks.withType(AbstractArchiveTask).configureEach {
preserveFileTimestamps = true
reproducibleFileOrder = false
}
}
$ for i in 1 2; do ./gradlew -q :tidepool-model:clean :tidepool-model:jar; shasum -a 256 tidepool-model/build/libs/tidepool-model-1.0.0.jar | cut -c1-16; sleep 2; done
875ec791c2960e71
875ec791c2960e71
$ for i in 1 2; do ./gradlew -q -PnonRepro :tidepool-model:clean :tidepool-model:jar; shasum -a 256 tidepool-model/build/libs/tidepool-model-1.0.0.jar | cut -c1-16; sleep 2; done
7230d90176fb608d
4f1a85a50f1d7522
The first pair of lines is identical with both DSLs (875ec791c2960e71 each time, in two separate builds in two projects with identical sources); with the switch on, the checksum changes on every build (the two
Groovy runs gave 4bd70f84d92097fe and 4c0164d51b4e07f6). Two things follow. If a build must produce a checksum that someone else can reproduce, do not turn the archive options back on; and what remains of non-determinism
is outside the archive settings: the JDK that compiled the classes (a toolchain pins it, see Gradle Toolchains), generated files that contain a timestamp or a host name, and the
order in which a custom task writes its output. The same advice for Maven is in Maven Security & Reproducible Builds, and publishing is in
Publishing.
Verify the wrapper
Two security practices concern the wrapper, which runs before any safeguard of your build: set distributionSha256Sum (added to the list in 9.1.0), and validate the wrapper on every upgrade (9.3.0). The JAR is a
binary that is checked into the repository, so a change to it deserves the review you would give to any executable. Gradle publishes the checksum of the wrapper JAR and of each distribution next to the release (https://gradle.org/release-checksums/, and
as plain text files at https://services.gradle.org/distributions/gradle-9.8.1-wrapper.jar.sha256 and ...-bin.zip.sha256). Compare the checksum of the checked-in JAR with the published one:
$ shasum -a 256 gradle/wrapper/gradle-wrapper.jar | cut -d' ' -f1
$ curl -sL https://services.gradle.org/distributions/gradle-9.8.1-wrapper.jar.sha256
3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98
3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98
The two values are equal, and equal to the one on the release-checksums page. The value of distributionSha256Sum
in the starter configuration, dce76f55...c01f, is the one published for the -bin zip. The second check recommended by the official page is to regenerate the wrapper with a Gradle that you trust and compare the files. Here a Gradle 9.8.1
installation (not the project’s wrapper) regenerated the wrapper in an empty project with gradle wrapper --gradle-version 9.8.1 --gradle-distribution-sha256-sum <sum> --no-validate-url, and cmp found gradlew, gradlew.bat and
gradle-wrapper.jar byte-identical to the ones in the project. The properties file differed only in distributionUrl, because the examples' wrapper downloads from a local file, not from services.gradle.org; the page
shows the official address. In a pull request, a change to the JAR or to distributionUrl that has no matching Gradle upgrade is a red flag. For GitHub Actions the official setup action (v4 or newer) validates every
gradle-wrapper.jar in the repository automatically (not run here); see CI & IDE Integration for the CI side.
The related practice is "Do not run ./gradlew on untrusted projects" (9.7.0): the wrapper script, the JAR and every build script execute arbitrary code, and opening a project in an IDE runs a Gradle sync that does the same. Read
the build scripts and gradle-wrapper.properties first, make sure distributionUrl points to https://services.gradle.org/, regenerate the wrapper with a trusted installation, or work in a container or a virtual machine.
The same applies to Maven’s wrapper and to Ant build files you downloaded; see Supply-Chain Security.
A note on adopting the list
Do not apply the list in one change. Adopt the cheap, mechanical items first (root project name, UTF-8, distributionSha256Sum, the plugins {} block), then the structural ones that touch many files
(catalog, convention plugins), and use the deprecation warnings (--warning-mode all, see Command-Line Interface) and the
configuration cache report to find the lazy-API and Project violations. The practice "Consider use of @Incubating APIs carefully" (9.7.0) cuts both ways: an incubating feature may change in a minor release,
yet some of them, such as dependencyResolutionManagement.repositories, are recommended by the list itself. Check when a feature was introduced and how many open issues it has, as the official page suggests;
the lifecycle labels are explained in Versions, Compatibility & Release History.
Troubleshooting
The Gradle troubleshooting guide is short; this section keeps its structure (installation, dependency resolution, slow builds, debugging build logic, tasks that run when they should be up to date, IDE integration, daemon connection) and adds the output you will actually see. Start by finding out where in the build the failure occurs, because the questions that follow differ.
The reason help is a good probe is that it runs the initialization and configuration phases (every settings and build script) and then one trivial task. If help works, the scripts are fine and the problem is in
the task that fails (see Build Lifecycle for the phases). If it fails, no task of yours has run yet.
Installation
./gradlew --version is the first thing to run (Kotlin DSL and Groovy DSL are irrelevant here). With the wrapper, this also downloads the distribution the first time.
$ ./gradlew --version
------------------------------------------------------------
Gradle 9.8.1
------------------------------------------------------------
Build time: 2026-10-07 16:57:46 UTC
Revision: 39daf39feb60a18b1b6ddccde53366522fef516f
Kotlin: 2.4.10
Groovy: 4.0.33
Ant: Apache Ant(TM) version 1.10.17 compiled on April 6 2026
Launcher JVM: 26 (Oracle Corporation 26+35-2893)
Daemon JVM: /Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home (no Daemon JVM specified, using current Java home)
OS: Mac OS X 27.0.1 aarch64
Three failures at this point, from the guide:
-
Command not found: gradle. Gradle was installed but its
bindirectory is not on thePATH; or, for a project, use./gradlew, which needs nothing installed (see Installation and Gradle Wrapper). Not run here. -
JAVA_HOMEis set to an invalid directory. The script checks the variable before starting anything:$ JAVA_HOME=/no/such/jdk ./gradlew --version ERROR: JAVA_HOME is set to an invalid directory: /no/such/jdk Please set the JAVA_HOME variable in your environment to match the location of your Java installation.Fix it by pointing
JAVA_HOMEat a JDK, or unsetting it. Gradle 9.8.1 needs Java 17 or newer to run the daemon (the wrapper client can still start on Java 8), but a project can still be compiled with other versions through toolchains, as in Gradle Toolchains. -
Permission denied. The script has lost its executable bit (this happens when a file is committed from Windows or unpacked from a ZIP archive). On the system used here (zsh), with the bit removed by
chmod -x gradlew:$ ./gradlew --version (eval):4: permission denied: ./gradlewThe message belongs to the shell (zsh here;
bashprintsPermission denied).chmod +x gradlewfixes it, andgit update-index --chmod=+x gradlewmakes the bit permanent in the repository.
If --version works but every build fails with the same error, a configuration script is broken. ./gradlew help runs those scripts and no real task:
$ ./gradlew help
Calculating task graph as no cached configuration is available for tasks: help
> Task :help
Welcome to Gradle 9.8.1.
To run a build, run gradlew <task> ...
To see a list of available tasks, run gradlew tasks
To see more detail about a task, run gradlew help --task <task>
To see a list of command-line options, run gradlew --help
For more detail on using Gradle, see https://docs.gradle.org/9.8.1/userguide/command_line_interface.html
For troubleshooting, visit https://help.gradle.org
BUILD SUCCESSFUL in 304ms
1 actionable task: 1 executed
Configuration cache entry stored.
A build script that does not work
Make a typing mistake in a project dependency (":tidepool-modle" instead of ":tidepool-model" in tidepool-engine/build.gradle.kts) and run help:
$ ./gradlew help
Calculating task graph as configuration cache cannot be reused because file 'tidepool-engine/build.gradle.kts' has changed.
FAILURE: Build failed with an exception.
* Where:
Build file '<work>/tidepool/tidepool-engine/build.gradle.kts' line: 6
* What went wrong:
Project with path ':tidepool-modle' could not be found.
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.
BUILD FAILED in 13s
Configuration cache entry stored.
(One line, Starting a Gradle Daemon, 3 busy and 47 stopped Daemons could not be reused, was removed; it describes the other daemons of the machine.) Read it in this order: Where names the file and the line (the line of the project(...) call,
not of the line that uses the result); What went wrong is the message; the Try: hints are generic. When the message is not enough, --stacktrace appends the exception and the full chain of calls (the first lines are shown here; the
whole trace is about two hundred lines of internal frames):
$ ./gradlew help --stacktrace
...
* Exception is:
org.gradle.api.UnknownProjectException: Project with path ':tidepool-modle' could not be found.
at org.gradle.api.internal.artifacts.DefaultProjectDependencyFactory.create(DefaultProjectDependencyFactory.java:73)
at org.gradle.api.internal.artifacts.DefaultProjectDependencyFactory.create(DefaultProjectDependencyFactory.java:67)
at org.gradle.api.internal.notations.ProjectDependencyFactory.create(ProjectDependencyFactory.java:41)
at org.gradle.api.internal.artifacts.DefaultDependencyFactory.createProjectDependency(DefaultDependencyFactory.java:134)
...
The frames that matter are the first one from org.gradle.api (what failed) and the last ones before Program.execute (the build script frame, which names build.gradle.kts:6). Script compilation errors are worse: in the Kotlin DSL a typo in a block name
(dependancies { ... }) reports Unresolved reference for the block and then for every call inside it, with the candidates of each overload, which can fill a screen. Look at the first error of the list only. The two DSLs are compared in
Kotlin DSL and Groovy DSL & Migrating to Kotlin. In the Groovy DSL the same mistake gives
the same Project with path ':tidepool-modle' could not be found message with Where: Build file '<work>/tidepool/tidepool-engine/build.gradle' line: 6, preceded by one more line in the What went wrong section, A problem occurred evaluating project ':tidepool-engine'.
Dependency resolution
Set the version of commons-csv in gradle/libs.versions.toml to 9.9.9, which does not exist, and compile. With the configuration cache on, the failure surfaces while Gradle stores the task graph and is wrapped in a long
Configuration cache state could not be cached message that contains the same text; to read the real one, run with --no-configuration-cache:
$ ./gradlew :tidepool-engine:compileJava --no-configuration-cache
> Task :tidepool-model:compileJava UP-TO-DATE
> Task :tidepool-engine:compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':tidepool-engine:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':tidepool-engine:compileClasspath'.
> Could not find org.apache.commons:commons-csv:9.9.9.
Searched in the following locations:
- https://repo.maven.apache.org/maven2/org/apache/commons/commons-csv/9.9.9/commons-csv-9.9.9.pom
Required by:
project ':tidepool-engine'
* Try:
> If the artifact you are trying to retrieve can be found in the repository but without metadata in 'Maven POM' format, you need to adjust the 'metadataSources { ... }' of the repository declaration.
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.
BUILD FAILED in 2s
2 actionable tasks: 1 executed, 1 up-to-date
The message says what was asked (org.apache.commons:commons-csv:9.9.9), by whom (Required by: project ':tidepool-engine'), which configuration (compileClasspath), and where Gradle looked (a single .pom URL, because the build
declares one repository). The causes are almost always one of: a typo in the version or the coordinates; a repository missing from dependencyResolutionManagement, or filtered out by content filtering
(Repositories & Content Filtering); a snapshot that was never published; a proxy or credentials problem (Gradle prints the HTTP status in the
message); or an artifact that exists without POM metadata, for which the Try: hint tells you to adjust metadataSources. The same build in offline mode shows the other side of the cache:
$ ./gradlew :tidepool-engine:compileJava --no-configuration-cache --offline
* What went wrong:
Execution failed for task ':tidepool-engine:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':tidepool-engine:compileClasspath'.
> Could not resolve org.apache.commons:commons-csv:9.9.9.
Required by:
project ':tidepool-engine'
> No cached version of org.apache.commons:commons-csv:9.9.9 available for offline mode.
* Try:
Offline mode uses only what is in the dependency cache; --refresh-dependencies forces the opposite (revalidate everything), and the cache, its expiry of dynamic versions and the lockfile and verification files
are explained in Dependency Locking, Verification & Caching.
When the build resolves but picks a version you did not expect, ask Gradle why with dependencyInsight. This is the same build with the correct version (1.14.1):
$ ./gradlew :tidepool-engine:dependencyInsight --configuration runtimeClasspath --dependency commons-csv
> Task :tidepool-engine:dependencyInsight
org.apache.commons:commons-csv:1.14.1
Variant runtime:
| Attribute Name | Provided | Requested |
|--------------------------------|--------------|--------------|
| org.gradle.status | release | |
| org.gradle.category | library | library |
| org.gradle.libraryelements | jar | jar |
| org.gradle.usage | java-runtime | java-runtime |
| org.gradle.dependency.bundling | | external |
| org.gradle.jvm.environment | | standard-jvm |
| org.gradle.jvm.version | | 21 |
org.apache.commons:commons-csv:1.14.1
\--- runtimeClasspath
A web-based, searchable dependency report is available by adding the --scan option.
The first block shows the variant that was selected and the attributes the consumer requested (Java 21, jar, java-runtime) against what the module provided; the second is the reverse tree, from the dependency to the
configuration that needs it. When there is a conflict, this report also lists the selection reasons (for example a conflict resolution, a constraint or a forced version), which is the quickest way to find the constraint, BOM, catalog or
substitution rule that decided; the mechanisms are in Dependency Resolution, Conflicts & Capabilities,
Resolution Rules & Substitution and
Variant-Aware Resolution & Module Metadata. The official guide also points to the Dependencies view of a Build Scan, which
offers the same information with a search box (publishing a scan was not run here). Maven’s equivalents (dependency:tree, -X) are in
Maven Troubleshooting & Best Practices.
A task that runs when it should be up to date
A task is skipped (UP-TO-DATE) when the values of its inputs, the contents of its input files and the state of its outputs equal those of the last execution. When a task runs although you changed nothing, --info prints
why it is not up to date. Two tasks demonstrate the usual causes. stampUnstable takes the current time as an input; stampStable takes the project version; noOutputs has an action and declares no output.
abstract class StampTask : DefaultTask() {
@get:Input
abstract val stamp: Property<String>
@get:OutputFile
abstract val stampFile: RegularFileProperty
@TaskAction
fun write() {
stampFile.get().asFile.writeText(stamp.get())
}
}
// Rerun trap 1: the input changes on every invocation.
tasks.register<StampTask>("stampUnstable") {
stamp = System.currentTimeMillis().toString()
stampFile = layout.buildDirectory.file("stamp-unstable.txt")
}
// The fix: derive the input from something that only changes when the project changes.
tasks.register<StampTask>("stampStable") {
stamp = project.version.toString()
stampFile = layout.buildDirectory.file("stamp-stable.txt")
}
// Rerun trap 2: an action but no declared outputs.
tasks.register("noOutputs") {
doLast { println("working") }
}
abstract class StampTask extends DefaultTask {
@Input
abstract Property<String> getStamp()
@OutputFile
abstract RegularFileProperty getStampFile()
@TaskAction
void write() {
stampFile.get().asFile.text = stamp.get()
}
}
// Rerun trap 1: the input changes on every invocation.
tasks.register('stampUnstable', StampTask) {
stamp = System.currentTimeMillis().toString()
stampFile = layout.buildDirectory.file('stamp-unstable.txt')
}
// The fix: derive the input from something that only changes when the project changes.
tasks.register('stampStable', StampTask) {
stamp = project.version.toString()
stampFile = layout.buildDirectory.file('stamp-stable.txt')
}
// Rerun trap 2: an action but no declared outputs.
tasks.register('noOutputs') {
doLast { println 'working' }
}
The build was run once to fill the outputs, and then again with --info, filtering the very long output down to the explanations. These runs used --no-configuration-cache: with the cache on, the value of
System.currentTimeMillis() is computed once, at configuration time, and stored in the cache entry, so a cache hit would freeze the "unstable" input and hide the problem (which is a different, quieter trap).
$ ./gradlew stampUnstable stampStable noOutputs --no-configuration-cache --info | grep -A1 -E "is not up-to-date|^Skipping task" | grep -v -E "^--|^Resolve mutations"
Task ':stampUnstable' is not up-to-date because:
Value of input property 'stamp' has changed for task ':stampUnstable'
Skipping task ':stampStable' as it is up-to-date.
Task ':noOutputs' is not up-to-date because:
Task has not declared any outputs despite executing actions.
The output is identical with the Groovy DSL. The two reasons seen here are the most frequent in practice:
-
A value of an input property changed. Look for timestamps, random numbers, build numbers, host names or absolute paths among the inputs. Move such data out of the inputs, make it an output, or take it from a stable source. Absolute paths also defeat the build cache, which is why the list of practices asks for
@PathSensitivedeclarations. -
The task declares no output. A task with actions and no declared output is always considered out of date, because Gradle cannot know what the task changed. Declare the outputs (or, for a task that only reports, accept that it runs).
Other reasons are reported in the same place: an input file that was added, removed or changed; an output file that was changed or removed between runs (including by another task that writes to
the same directory); and an explicit request to rerun (--rerun for one task, --rerun-tasks for all of them). The vocabulary and the inputs-and-outputs model are in
Incremental Build; the meaning of the outcomes (UP-TO-DATE, FROM-CACHE, SKIPPED, NO-SOURCE) is in the command-line page. The Timeline view of a Build Scan gives the same
explanation as a searchable page (not run here). A dry run shows what would run, in order, without running anything:
$ ./gradlew :tidepool-cli:jar -m
Calculating task graph as configuration cache cannot be reused because file 'build.gradle.kts' has changed.
:tidepool-model:compileJava SKIPPED
:tidepool-engine:compileJava SKIPPED
:tidepool-cli:compileJava SKIPPED
:tidepool-cli:processResources SKIPPED
:tidepool-cli:classes SKIPPED
:tidepool-cli:jar SKIPPED
BUILD SUCCESSFUL in 1s
Configuration cache entry stored.
Slow builds
The troubleshooting guide sends slow builds to the performance guide, which is the page Performance Profiling & Build Scans. A local first step that needs no
account is --profile, which writes an HTML report with the time of each phase, each project and each task:
$ ./gradlew clean build --profile
...
Configuration cache entry stored.
See the profiling report at: file://<work>/tidepool/build/reports/profile/profile-2026-10-16-21-24-13.html
A fine-grained performance profile is available: use the --scan option.
The report is a file under build/reports/profile/. The scan option (--scan) is more detailed, but it publishes the build to an external service after you accept its terms of use, so it was not run here. Most of the time lost in practice
comes from the practices of this page that are about performance: no configuration cache (configuration repeated on every invocation), no build cache, expensive computation in the configuration phase, dependency resolution during configuration (the
practice "Don’t resolve Configurations before Task Execution"), and an undersized or oversized daemon heap (The Gradle Daemon). Build output as a stream of
> Task ... lines is lifecycle logging; --console=verbose keeps outcomes such as UP-TO-DATE on the console in an interactive terminal.
Debugging build logic
Build logic (a script, buildSrc, a plugin of an included build) is code, and it can be debugged like code. Set the org.gradle.debug property to true and the build starts a JVM that suspends until a remote debugger attaches to port 5005
(org.gradle.debug.port changes the port; org.gradle.debug.host sets the interface, and * listens on all of them, which exposes the build to the network, so use it only when you must). The Kotlin DSL scripts can be debugged too, not only the plugin classes.
This run used a Gradle User Home created for it (so that the shared daemons were not touched) and was ended after the line appeared, since no debugger was attached (an IDE attach was not run here):
$ ./gradlew help -Dorg.gradle.debug=true
...
Starting a Gradle Daemon (subsequent builds will be faster)
Listening for transport dt_socket at address: 5005
The JVM listens on 127.0.0.1:5005 only (confirmed with lsof in this run), and then waits. In an IDE, create a Remote JVM Debug configuration for that port and set breakpoints in the plugin or script source. The JVM stays suspended until a debugger attaches or
you kill it (here the daemon and the client were ended by killing their processes).
For logs, raise the level (--info, --debug, --warning-mode all), and remember that the daemon writes its own log to $GRADLE_USER_HOME/daemon/<version>/daemon-<pid>.out.log. If the build fails
before any task, with a message that a new daemon could not be connected to, the guide attributes it to network address translation (NAT) masquerading local connections as external ones, which Gradle refuses as a security precaution;
the fix is in the network configuration, and the daemon log shows the detected network setup (not run here). Directories, log levels and the log format are in
Directories & Logging.
Debugging IDE integration
Many rare IDE errors disappear when the IDE re-imports the build: in IntelliJ IDEA open the Gradle tool window and use the Refresh icon (the project must be linked to Gradle); in Eclipse with Buildship use the Refresh action of the "Gradle Tasks" view or the Gradle > Refresh Gradle Project command (both not run here). The IDE runs the same Gradle with the same wrapper, so a build that works on the command line and fails in the IDE usually differs in the JDK used for Gradle, in a Gradle User Home, or in the run configuration; see CI & IDE Integration and Init Scripts & the Tooling API.
Symptom, first command, where to read on
| Symptom | First thing to run | Page |
|---|---|---|
|
|
|
Every build fails the same way, before any task |
|
|
|
|
|
The wrong version of a dependency wins |
|
|
A task always runs |
|
|
|
Read the configuration cache report; capture values at configuration time |
|
The build is slow |
|
|
|
|
|
A build breaks after an upgrade |
|
|
A plugin or task misbehaves |
|
The same ideas in Ant and Maven
| Need | Gradle | Maven | Ant |
|---|---|---|---|
Verbose output |
|
|
|
Dry run |
|
none for the whole build (a plugin may offer one) |
no dry-run option |
Why did it run? |
|
every goal runs; the incremental logic is in each plugin |
a task decides by itself (timestamps in |
Why this dependency version? |
|
|
Ivy resolve output and reports, see the Ivy pages |
Pin the toolchain |
Java toolchains |
|
set |
The Maven row is detailed in Maven Troubleshooting & Best Practices; the Ant options in Running Ant; the three tools side by side in Comparing Ant, Maven and Gradle.
What differs in Gradle 8.14
Not run here; the sources are the release notes and the upgrade guides.
-
The oldest entries of the best-practices list are marked 8.14; entries with later "added in" versions describe advice that the 8.14 documentation does not contain, though most of it applies to 8.x builds.
-
Archive tasks are reproducible by default only from 9.0.0 (the two properties above must be set by hand in 8.14).
-
Error messages gain the task provenance (
registered in build file ...) in 9.5.0. -
The Gradle daemon needs Java 8 to run in 8.14 and Java 17 in 9.0, so
JAVA_HOMEproblems that mean "too old Java" are new with 9. -
The configuration cache is stable since 8.1, so the
Task.projectfailure above is the same in 8.14; what 9.x adds is the recommendation to use it by default (still not the default; announced for Gradle 10).
The sources are in Versions, Compatibility & Release History and Upgrading Gradle.
Where the book is outdated
The requester-provided book Gradle in Action (Muschko, 2014) was written for Gradle 1.7. Advice in it that the current best practices replace:
| The book says | Today |
|---|---|
Share configuration with |
Convention plugins in |
Versions in |
A version catalog (stable since 7.4) |
Use the project freely at execution time; no configuration cache |
The configuration cache is stable since 8.1; |
|
The |
|
Java toolchains |
Groovy DSL only |
Kotlin DSL is recommended for new builds and is the default of |
The daemon is opt-in ( |
On by default since 3.0 |
Plugin tests with |
TestKit |