ClassNotFoundException in Java: Fixing Missing Classpaths
Java throws ClassNotFoundException when Class.forName can't find a class on the classpath.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- ✓Basic Java compilation with javac and java
- ✓Familiarity with JAR files
- ✓A Maven or Gradle project to inspect
- The runtime classpath lacks the named class: a missing JAR, a wrong -cp flag, or a misspelled name in Class.forName. Read the class name from the trace first.
- Code that compiles can still throw, because reflective lookups read strings the compiler never checks and the runtime searches fewer JARs.
- JDBC failures mean the driver JAR never shipped; check Maven scope, since provided and test never reach production.
- Confirm with jar tf that a JAR holds the class, fix the scope or the flag, and smoke-test the packaged artifact.
Imagine a librarian who fetches books only by call number. At catalog time you list every book you'll need, and the library stocks them. But one request names a book nobody shelved, so the librarian searches every aisle and reports it missing. That failed search is ClassNotFoundException. A misspelled number, a book never ordered, or a delivery left on the wrong dock all end the same way. The fix is never arguing with the librarian; it's shelving the right book under the number on the note.
Your code compiles, your IDE shows no red, and the moment it runs the JVM reports it can't find a class you clearly wrote. ClassNotFoundException is the runtime's way of saying the classpath doesn't contain what the code asked for. The class existed at compile time or lives only in a string, and the runtime search across JARs and directories came up empty.
It fires in a small set of situations. Code calls Class.forName with a name nothing on the classpath provides. A JDBC driver class is loaded without its JAR deployed. A dependency is scoped provided or test in Maven, so it compiles but never ships. A launch script sets -cp to the wrong directories. Each one ends with the same exception naming the missing class.
The confusion comes from the gap between compiling and running. Compilation resolves the classes you reference directly against compile dependencies. But reflective lookups read class names from strings the compiler never checks, and the runtime searches a different, often smaller set of JARs. Clean compile plus failed launch is the signature of this gap.
This guide walks the lookup mechanism, the missing-JAR cases, JDBC loading, the NoClassDefFoundError distinction, classpath repair with -cp and build tools, and stack-trace reading. You'll learn to turn the missing class name into a short hunt instead of a long outage.
Classpath Lookup at Runtime: How forName Finds Classes
Class loading by name is a search, not a reference. When code calls Class.forName with a driver or plugin name, the JVM asks the classloader to find bytes for that exact name across every classpath entry. The loader scans directories and JARs in order, and the first entry holding the class file wins. When no entry contains it, the loader throws ClassNotFoundException carrying the name it couldn't find.
The classpath is the search path, and the -cp flag defines it at launch. Entries can be directories of compiled classes, individual JAR files, or wildcards like lib/* covering every JAR in a directory. The default classpath is the current directory only, which surprises developers whose IDE silently assembles a rich path behind the scenes. Launch the same code outside the IDE and classes outside the current directory vanish from the search.
Classloaders form a chain, and each loader asks its parent first. Application code usually loads through the application classloader, which sees the -cp entries. Containers add more loaders for shared libraries and web apps, and a class visible to one loader can be invisible to another. The same lookup can therefore succeed in a standalone run and fail inside a server, with identical JARs on disk.
Because the name arrives as a string, the compiler never verifies it. A typo compiles cleanly, a renamed class compiles cleanly, and an upgrade that moves a class compiles cleanly. Every one of them fails at runtime with the stale name in the message. Treat the exception text as the primary evidence: it names exactly what the loader sought.
Missing JARs and Dependencies That Cause It
A missing JAR is the most common cause, and it usually means the dependency never shipped rather than never existed. The code compiled against the library, tests ran against it, and the packaged app left it behind. Maven scopes provided and test both exclude the JAR from the artifact by design, and a hand-built launch script simply forgets entries. The result is identical: the loader searches, finds nothing, and names the class.
Version drift produces the same exception with a twist. The JAR ships, but it's an older release from before the class moved packages or the driver renamed its entry point. The loader finds the JAR, scans it, and still reports the class missing. Developers then insist the dependency is present because the file exists, while the class inside belongs to a different era. Only inspecting the JAR contents settles it.
Fat-JAR assembly adds a third variant. Shade plugins merge many JARs into one, and overlapping files silently overwrite each other. A needed class can vanish when two dependencies bundle the same path, or relocation rules can rename it out from under the lookup string. The build log claims success while the artifact quietly lacks the bytes.
Confirm contents before changing config. Running jar tf against each candidate JAR shows whether the class file exists anywhere. If nothing contains it, add or upgrade the dependency. If something contains it, the launch classpath is wrong. That one check splits the problem space in half and stops the guessing.
JDBC Driver Loading and the Forgotten JAR
JDBC made Class.forName famous. Drivers register with DriverManager, and older drivers required an explicit forName call with the driver class before connecting. That call throws when the driver JAR sits outside the runtime classpath, which happens constantly: the dependency is test-scoped, the app server expects it in shared lib, or the container image dropped it. The exception names the driver class, pointing straight at the missing JAR.
Modern drivers self-register through the service-loader file META-INF/services/java.sql.Driver inside the driver JAR. Current code can call DriverManager.getConnection directly with no forName at all. But self-registration still needs the JAR present: without it, there is no services file to discover and getConnection fails with no suitable driver instead. The mechanism changed, the deployment requirement didn't.
Driver version mismatches add confusion. Legacy and current driver class names coexist in migration guides, and tutorials mix them freely. Loading an outdated name against a new driver JAR throws even though a driver is present, because that exact class no longer exists. Copy the driver class name from the driver's own documentation for the version you ship.
Fix driver issues at the dependency level. Declare the driver with compile scope so it ships inside the artifact, verify with dependency:tree, and confirm the packaged JAR holds the driver classes. Then let self-registration work and keep an explicit forName only where legacy code demands it. One scoped dependency done right ends the whole category.
ClassNotFoundException Versus NoClassDefFoundError
These two errors sound alike and mean different things. ClassNotFoundException is an exception thrown when a reflective lookup by name finds nothing: Class.forName, ClassLoader.loadClass, or a service-loader scan. The lookup ran, the search failed, and the exception carries the name. It is a load-time event about a search that came up empty.
NoClassDefFoundError is an error thrown when the JVM links code that was compiled against a class it can no longer find. Compilation succeeded with the class present; at runtime the linker resolves the reference and discovers the bytes are gone. It is a link-time event about a promise the deployment broke. The class name appears too, but the story is a vanished dependency, not a failed search.
The practical difference is where you look. ClassNotFoundException sends you to the lookup: check the name string, check who calls forName, check the loader's visibility. NoClassDefFoundError sends you to the deployment: diff the build classpath against the runtime classpath and find the JAR that compiled but never shipped. Applying lookup fixes to a deployment gap wastes hours.
They often arrive together. A failed forName inside a static initializer can cascade into linkage errors for every class that touches the failed one. Read the first exception in the log, not the loudest: the initial ClassNotFoundException names the root missing class, and everything after it is fallout from the same absent JAR.
Fixing the Classpath With -cp, Maven, and Gradle
Repair starts with seeing the actual classpath, not the one you assume. The -cp flag sets it explicitly and overrides the CLASSPATH environment variable completely, so a stale CLASSPATH never merges with your flag. Quote wildcards like 'lib/*' so the JVM expands them instead of the shell, and remember the separator is a colon on Linux and macOS but a semicolon on Windows. Print the classpath property from the running app to see what the loader truly searches.
In Maven, dependency scope decides what ships. Compile scope travels into the artifact; provided and test do not. A driver or library marked provided must be supplied by the container, and after any migration that supplier may be gone. Run mvn dependency:tree to see scopes, change what production needs to compile, and rebuild with clean package so stale artifacts can't linger.
In Gradle, the same split appears as implementation versus compileOnly. CompileOnly dependencies vanish from the runtime exactly like Maven's provided. Run gradle dependencies to inspect the graph, move runtime needs to implementation, and rebuild. Both tools reward the same habit: review scope changes in pull requests with the seriousness of code changes.
Finish by testing the artifact, not the source tree. Launch the packaged JAR with the production launch flags and exercise the loading paths: driver startup, plugin scans, reflective factories. Tests run on a richer classpath and will keep passing while the artifact stays broken, so the packaged smoke test is the only verdict that counts.
Reading the Stack Trace for the Missing Class Name
The stack trace hands you the answer in its first line: the fully qualified name of the missing class. It tells you the package, the library family, and the exact lookup string in one breath. Copy that name verbatim into your search; retyping it risks fixing a typo with another typo. The frames below show which call triggered the lookup, distinguishing your forName call from a framework scan.
Next, locate the name in your dependency graph. Search the source for the string to find explicit forName calls, configuration files naming driver classes, and plugin descriptors. Then search the binaries: run jar tf across the runtime JARs and grep for the class file path, which is the package with slashes plus .class. Finding it in a JAR that isn't on the launch path proves a classpath bug; finding it nowhere proves a missing dependency.
Watch for the caused-by chain. Frameworks wrap the original ClassNotFoundException in servlet, Spring, or loader exceptions, burying the missing name several Caused by lines deep. Scroll to the last caused-by: the root exception names the class, while the wrappers describe the load path through containers and initializers. The outermost message tells you the feature that broke; only the innermost names the fix.
Record the loader when containers are involved. Messages that include the classloader identity or the module layer tell you which visibility domain failed. Two identical JARs on disk mean nothing if the calling loader can't see the right one. Note the loader, check its library list, and fix visibility instead of adding duplicate JARs that only create shadowing conflicts.
One Maven Scope Word Kept the Database Driver Out for 3 Hours
- Dependency scope is a deployment decision, not just a build detail. Every provided scope needs a named supplier, rechecked whenever the runtime changes.
- Readiness probes must exercise real startup paths. A probe that opens the port without loading the driver reports healthy while every request fails.
- Test the artifact you ship. A suite running on the test classpath cannot catch JARs that never make it into the package.
| File | Command / Code | Purpose |
|---|---|---|
| LookupDemo.java | public class LookupDemo { | Classpath Lookup at Runtime |
| DriverLoad.java | public class DriverLoad { | Missing JARs and Dependencies That Cause It |
| JdbcConnect.java | public class JdbcConnect { | JDBC Driver Loading and the Forgotten JAR |
| ShowClasspath.java | public class ShowClasspath { | Fixing the Classpath With -cp, Maven, and Gradle |
Key takeaways
Common mistakes to avoid
6 patternsRunning java without -cp and expecting it to see your JARs
Marking the JDBC driver dependency as provided scope
Loading a driver or class by a half-remembered name
Compiling against a library that never ships to production
Treating ClassNotFoundException and NoClassDefFoundError as the same bug
Building a fat JAR that silently drops a dependency
Interview Questions on This Topic
What triggers a ClassNotFoundException?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Exception Handling. Mark it forged?
6 min read · try the examples if you haven't