These two look alike and both say “the class is not there”, but they come from different situations. The first is a checked exception thrown when code asks for a class by name:
Exception in thread "main" java.lang.ClassNotFoundException: org.postgresql.Driver
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:421)
at java.base/java.lang.Class.forName(Class.java:412)
at com.example.App.main(App.java:7)The second is an Error thrown when a class that was present at compile time cannot be used at runtime. Note the slash-separated internal name:
Exception in thread "main" java.lang.NoClassDefFoundError: com/google/gson/Gson
at com.example.App.main(App.java:12)
Caused by: java.lang.ClassNotFoundException: com.google.gson.Gson
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
... Line numbers inside the JDK vary by release. What matters is which of the two you have, the class name, and the first frame in your own code.
Quick fix checklist
ClassNotFoundException: look at the name passed toClass.forName,loadClassor a config file (JDBC driver, logging backend, plugin class). Either the name is misspelled or the jar is not on the runtime classpath.NoClassDefFoundError: a/b/CwithCaused by: ClassNotFoundException: the dependency compiled fine but is not packaged or not on the runtime classpath. Check its Maven or Gradle scope.NoClassDefFoundError: Could not initialize class a.b.C: the class exists, but its static initializer threw earlier. Search the logs for the firstExceptionInInitializerError.- Run
jar tf app.jar | grep Gsonto see whether the class is inside your artifact. - Run
mvn dependency:tree -Dincludes=com.google.code.gson(or Gradle’sdependencyInsight) to see which version and scope you resolve. - If you launch with
java -jar, remember that-cpis ignored.
Before you start
You should know what the classpath is: the list of directories and jar files the application class loader searches for .class files. You will need access to the built artifact and the exact command used to start it (look in the Dockerfile, systemd unit or launch script, not just your IDE). The examples use Maven; Gradle equivalents are given where they differ.
Why it happens
Java loads classes lazily. The compiler checks that Gson exists when you compile, but the JVM only goes looking for com/google/gson/Gson.class the first time your running code touches it. Between those two moments the world can change: a different classpath, a jar left out of the package, a conflicting version.
ClassNotFoundException is thrown by the class loading API itself when you ask for a class by a string name: Class.forName("org.postgresql.Driver"), ClassLoader.loadClass, or a framework doing the same with a name from configuration. It is a checked exception because the caller explicitly asked for something that might not exist and is expected to handle that.
NoClassDefFoundError is thrown by the JVM when it resolves a symbolic reference compiled into a class file (a new Gson(), a method call, a field type) and cannot produce a usable class. It is a LinkageError, so it is unchecked and usually fatal for that code path. There are two common causes:
- The class file cannot be found. The JVM tried to load it and the loader threw
ClassNotFoundException, which appears as theCaused by. - The class was found but its static initialization failed. The first attempt throws
ExceptionInInitializerErrorwith the real cause. The class is then marked as erroneous, and every later use throwsNoClassDefFoundError: Could not initialize class .... On JDK 18 and later that later error carries a cause recording the original exception; on older JDKs you must find the first failure in the logs.
Step-by-step walkthrough
Step 1: Classify the error
Read the first line. If it is ClassNotFoundException, find the string that was being loaded and where it came from. If it is NoClassDefFoundError, check whether the message is a class name (missing class) or starts with Could not initialize class (failed initializer). These lead to completely different fixes.
Step 2: Prove what is on the runtime classpath
Do not trust the IDE, which builds its own classpath. Inspect what actually runs:
# Is the class inside the jar you deploy?
jar tf target/app.jar | grep 'com/google/gson/Gson.class'
# Spring Boot fat jars keep dependencies under BOOT-INF/lib
jar tf target/app.jar | grep 'BOOT-INF/lib/gson'
# What does the manifest say?
unzip -p target/app.jar META-INF/MANIFEST.MFA plain jar built by maven-jar-plugin contains only your classes. Running it with java -jar app.jar finds dependencies only through the manifest’s Class-Path entry; both the -cp option and the CLASSPATH environment variable are ignored in that mode. To run it, either list everything explicitly with java -cp "app.jar:lib/*" com.example.App or build an executable fat jar (Spring Boot plugin, maven-shade-plugin or maven-assembly-plugin).
To see where each class is really loaded from, run with class loading logs:
java -Xlog:class+load=info -jar app.jar | grep gsonStep 3: Check dependency scope and versions
mvn dependency:tree -Dincludes=com.google.code.gson[INFO] com.example:app:jar:1.0.0
[INFO] \- com.google.code.gson:gson:jar:2.11.0:providedThe scope at the end is the clue. provided means “the runtime environment will supply this” (true for the Servlet API in Tomcat, false for Gson in a standalone jar), so it is not packaged. test means the dependency is only on the test classpath. Code under src/main cannot compile against it, but anything loaded by name (a JDBC driver or logging backend declared test-only) passes the tests and then throws ClassNotFoundException in production. Gradle’s equivalents are compileOnly and testImplementation:
./gradlew dependencyInsight --dependency gson --configuration runtimeClasspathStep 4: Look for version conflicts and shading
Sometimes the jar is there but the wrong version is. Library A was compiled against guava 32, which has a class that guava 19 lacks; Maven’s “nearest wins” rule picked 19 for your build. Library A then throws NoClassDefFoundError (or NoSuchMethodError) for a class it compiled against. mvn dependency:tree shows which version won and through which path. Fix it by declaring the version you need in dependencyManagement. Shaded jars that bundle their own copy of a library under the original package name cause the same symptom; prefer artifacts that relocate shaded packages.
Step 5: Fix failed static initializers at the source
For Could not initialize class, the missing class is a red herring. Scroll back to the first occurrence:
java.lang.ExceptionInInitializerError
at com.example.PaymentClient.charge(PaymentClient.java:22)
Caused by: java.lang.IllegalStateException: PAYMENT_URL is not set
at com.example.Config.<clinit>(Config.java:9)<clinit> is the static initializer. Fix the cause (here a missing environment variable), and consider moving fragile work out of static initialization so failures are reported with context and can be retried.
Worked scenario
A nightly report job is a plain Java application, packaged with maven-shade-plugin into a single jar and started by cron with java -jar report.jar. After a refactor moved the JSON code into a shared module, the job fails on its first run:
Exception in thread "main" java.lang.NoClassDefFoundError: com/google/gson/Gson
at com.example.json.JsonSupport.toJson(JsonSupport.java:31)
at com.example.report.ReportWriter.write(ReportWriter.java:44)
at com.example.report.Main.main(Main.java:18)
Caused by: java.lang.ClassNotFoundException: com.google.gson.Gson
...All unit tests passed, and the build was green. Diagnosis starts with the artifact: jar tf report.jar | grep gson prints nothing, so the class really is absent. Running mvn dependency:tree -Dincludes=com.google.code.gson in the report module also prints nothing, which is suspicious for a library the code clearly uses. The top frame, JsonSupport.toJson, lives in the shared module, so the next step is that module’s tree:
[INFO] com.example:json-support:jar:1.4.0
[INFO] \- com.google.code.gson:gson:jar:2.11.0:providedThe shared module’s pom.xml had been copied from a web application where the server supplied Gson:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
<scope>provided</scope>
</dependency>Now everything lines up. json-support compiled against Gson and its tests passed, because Maven’s compile and test classpaths include provided dependencies. provided dependencies are not transitive, so the report module never received Gson, and it compiled anyway because it only calls JsonSupport, never Gson directly. The shade plugin packages the runtime classpath (compile and runtime scopes), so the jar shipped without Gson. The first call into toJson resolved the reference to com/google/gson/Gson and failed.
The fix is to remove the <scope>provided</scope> line so Gson becomes a normal compile dependency of json-support, then rebuild and confirm with jar tf report.jar | grep 'com/google/gson/Gson.class'. The job runs.
Common mistake
The common mistake is treating both errors as “add the jar somewhere”. Copying jars into lib/ by hand, or into the JDK’s directories, makes the build unreproducible and often introduces a second version of the library, which creates the conflict described in Step 4.
For Could not initialize class, people add the jar again or restart repeatedly without reading earlier logs. The class is present; its initializer is failing, and only the first ExceptionInInitializerError tells you why.
Catching NoClassDefFoundError to fall back silently is also risky. It is an Error; after a failed static initializer, that class stays unusable for the life of the class loader.
Verify the behavior
Test the packaged artifact, not just the IDE run. A simple smoke step in CI catches missing runtime classes:
mvn -q package
jar tf target/report.jar | grep -q 'com/google/gson/Gson.class' && echo "gson packaged"
java -jar target/report.jar --dry-run # assumes the job has a no-side-effects modeFor reflective loading, a JUnit test documents what must be on the runtime classpath:
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class RuntimeClasspathTest {
@Test
void jdbcDriverIsAvailable() {
assertDoesNotThrow(() -> Class.forName("org.postgresql.Driver"));
}
@Test
void unknownClassIsReportedAsClassNotFound() {
assertThrows(ClassNotFoundException.class, () -> Class.forName("org.example.DoesNotExist"));
}
}Remember that Maven runs tests with the test classpath, which includes provided and test scopes, so this test cannot catch a scope problem on its own; the packaged smoke run can.
Interview exercise
What is the difference between ClassNotFoundException and NoClassDefFoundError, and can you describe a case where NoClassDefFoundError occurs even though the class file is definitely on the classpath?
Answer and reasoning
ClassNotFoundException is a checked exception from explicit, name-based loading (Class.forName, ClassLoader.loadClass): the caller asked for a class that might not exist. NoClassDefFoundError is a LinkageError thrown when the JVM resolves a reference that was compiled into bytecode and cannot get a usable class at runtime, often with a ClassNotFoundException as its cause. The case with the file present is a failed static initializer: the first access throws ExceptionInInitializerError, the JVM marks the class as erroneous, and every later access throws NoClassDefFoundError: Could not initialize class. A good answer adds the debugging consequence: you must find the first error in the logs (or, on JDK 18 and later, read the attached cause), because the later errors no longer say what went wrong.