Home Java ClassNotFoundException in Java: Fixing Missing Classpaths
Intermediate 6 min · September 23, 2026

ClassNotFoundException in Java: Fixing Missing Classpaths

Java throws ClassNotFoundException when Class.forName can't find a class on the classpath.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 12 min
  • Basic Java compilation with javac and java
  • Familiarity with JAR files
  • A Maven or Gradle project to inspect
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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.
✦ Definition~90s read
What is Java ClassNotFoundException Fix?

ClassNotFoundException is the checked exception Java throws when a runtime lookup for a class by name finds nothing on the classpath. Lookups happen through Class.forName, ClassLoader.loadClass, and service-loader scans that read class names from strings or configuration.

Imagine a librarian who fetches books only by call number.

The calling classloader searches its classpath entries in order, and when none contains the requested bytes it throws with the missing name in the message. Callers must catch or declare it, since it is checked.

The mechanism is deliberately separate from compilation. The compiler resolves every class you name directly against compile-scope dependencies and rejects unknown ones before any bytecode exists. But names inside strings, config files, and annotations are opaque to the compiler: a forName call with any string compiles no matter what it holds.

The runtime then searches a classpath that may be smaller, differently ordered, or assembled by a different tool than the compile path. That gap between two different class sets is where this exception lives.

What it is NOT clarifies the fix. It is not a syntax or type error: the code is well-formed. It is not NoClassDefFoundError, which is a link-time error about a class that compiled but vanished at runtime. It is not a version conflict by itself, though stale JARs cause it.

And it is not fixed by cleaning and rebuilding when the dependency was never declared: a fresh build of an incomplete graph stays incomplete.

Think of the classpath as the library's shelves and forName as a call slip with a call number. If the number is misspelled, the book was never ordered, or it sits in a closed stack your pass can't enter, the desk reports it missing. Reshelve the book, fix the number, or widen access, and the next lookup succeeds.

Plain-English First

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.

LookupDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LookupDemo {
    public static void main(String[] args) {
        String[] names = {"java.util.ArrayList", "com.example.Missing"};
        for (String n : names) {
            try {
                Class<?> c = Class.forName(n);
                System.out.println("found: " + c.getName());
            } catch (ClassNotFoundException e) {
                System.out.println("missing: " + e.getMessage());
            }
        }
    }
}
📊 Production Insight
Standalone success plus container failure almost always means loader visibility, not missing files. Check which loader owns the call and which libraries each loader sees before rebuilding anything.
🎯 Key Takeaway
forName searches classpath entries in order through a loader chain, and the first match wins. Names arrive as unchecked strings, so the exception message is the authoritative record of what was 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.

DriverLoad.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
public class DriverLoad {
    public static void main(String[] args) {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            System.out.println("driver loaded");
        } catch (ClassNotFoundException e) {
            System.out.println("driver JAR missing from classpath: " + e.getMessage());
        }
    }
}
📊 Production Insight
Provided scope is the repeat offender in migrated services. The old container supplied the JAR, the new one doesn't, and nothing fails until the first lookup. Audit every provided scope after any runtime migration.
🎯 Key Takeaway
Most missing classes trace to JARs that never shipped: excluded scopes, forgotten -cp entries, stale versions, or shade collisions. Prove it with jar tf before changing anything.

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.

JdbcConnect.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
import java.sql.Connection;
import java.sql.DriverManager;

public class JdbcConnect {
    public static void main(String[] args) throws Exception {
        String url = "jdbc:mysql://localhost:3306/shop";
        try (Connection c = DriverManager.getConnection(url, "app", "secret")) {
            System.out.println("connected: " + !c.isClosed());
        }
    }
}
📊 Production Insight
Connection pools defer loading until the first checkout, so a missing driver surfaces as slow failures under traffic rather than a fast crash at boot. Load one connection during startup to convert this into an instant, obvious failure.
🎯 Key Takeaway
Driver loading throws when the driver JAR isn't deployed, regardless of explicit or self-registration. Ship it with compile scope, match the class name to your driver version, and verify the packaged artifact.

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.

🔥Read the Exception Name Before Acting
When both appear in one incident, fix the ClassNotFoundException first. It names the class the loader sought. The NoClassDefFoundError that follows is usually collateral from the same missing JAR.
📊 Production Insight
Log aggregators that deduplicate by class name merge these two into one alert and hide the distinction. Keep the exception type in the alert title so responders know whether they're hunting a name or a deployment.
🎯 Key Takeaway
The former is a failed load-time search by name; the latter is a broken link-time promise from a class that compiled but never shipped. Fix lookups for the first, deployments for the second, and read the earliest exception.

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.

ShowClasspath.javaJAVA
1
2
3
4
5
6
7
8
9
10
public class ShowClasspath {
    public static void main(String[] args) {
        String cp = System.getProperty("java.class.path");
        for (String entry : cp.split(System.getProperty("path.separator"))) {
            System.out.println(entry);
        }
        System.out.println(ShowClasspath.class.getResource("ShowClasspath.class"));
    }
}
📊 Production Insight
Hand-rolled launch scripts are where classpaths rot: someone adds a Maven dependency and forgets the script. Generate the classpath from the build with dependency:build-classpath instead of maintaining entries by hand.
🎯 Key Takeaway
Print the effective classpath from the running app, ship production needs in compile or implementation scope, and smoke-test the packaged artifact with production flags.

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.

📊 Production Insight
Wrapped traces mislead triage into debugging the framework instead of the missing JAR. Train responders to jump to the last caused-by first; the framework frames above it are context, not suspects.
🎯 Key Takeaway
The first line names the missing class; jar tf across runtime JARs decides between classpath bug and missing dependency. Read the innermost caused-by and note the loader in containers.
● Production incidentPOST-MORTEMseverity: high

One Maven Scope Word Kept the Database Driver Out for 3 Hours

Symptom
After a container migration, all 12 instances threw ClassNotFoundException for the MySQL driver class on their first database query. The service stayed up and answered health checks, so the load balancer kept sending traffic into errors for 3 hours before the rollback.
Assumption
The team assumed provided scope was correct because the driver had always been supplied by the old server image. Nobody rechecked that assumption after the container migration, and tests kept passing on the richer test classpath.
Root cause
The MySQL driver dependency was scoped provided, a leftover from servers that supplied the driver in shared lib. The new container image supplied nothing, so com.mysql.cj.jdbc.Driver was absent at runtime and all 12 instances threw ClassNotFoundException on their first query. The old image had masked the mis-scoping for two years.
Fix
The driver scope changed from provided to compile in the service's pom, and the image build gained a check: jar tf app.jar | grep -c 'mysql' must print at least 40 class files or the build fails. Connection startup moved into the readiness probe, so a missing driver blocks traffic instead of serving errors. Rollback completed in 22 minutes; the permanent fix shipped the same evening.
Key lesson
  • 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.
Production debug guideFive hunts that turn the missing class name into a found JAR.5 entries
Symptom · 01
Exception names a class you don't recognize
Fix
Read the fully qualified name after the colon; that exact string is your search key. Search the codebase for it: grep -rn 'com.example.Missing' src/. If it appears only inside a Class.forName string, the name was never compiler-checked and a typo is likely. Copy the name from the trace rather than retyping it.
Symptom · 02
You need to know whether the JAR or the launch is at fault
Fix
List what each runtime JAR actually contains: jar tf lib/app.jar | grep 'Missing.class'. Walk every entry on the launch classpath the same way. If no JAR contains the class file, the dependency never shipped. If one does, the launch classpath is wrong and the -cp flag needs repair.
Symptom · 03
Tests pass but production throws on boot
Fix
Run mvn dependency:tree -Dincludes=groupId:artifactId and check the scope column. Provided and test scopes never reach the packaged app. Change the scope to compile, rebuild with mvn clean package, and confirm with jar tf target/app.jar that the classes are inside.
Symptom · 04
Runs in the IDE but fails from the command line
Fix
Reproduce with an explicit classpath: java -cp 'lib/:classes' com.example.Main. Then print the effective path from inside the app with System.getProperty("java.class.path"). Diff the two: shell globbing, relative directories, and a forgotten CLASSPATH override are the usual gaps. Quote lib/ so the JVM expands it, not the shell.
Symptom · 05
Same classpath works standalone but fails in the container
Fix
Check the container's loader order: shared-lib JARs load before app JARs in many servers, so an older copy in shared lib shadows your new class. Move the authoritative JAR to one location, remove duplicates, and redeploy. Log each loader's URLs at startup for loaders you don't control.
ClassNotFoundException Causes Compared
Root CauseHow to ConfirmFixPrevention
JAR missing from the runtime classpathjar tf on each classpath entry shows no such class fileAdd the JAR via -cp or the build file and redeployAssert deployment lib contents against dependency:tree in CI
Misspelled or outdated class name stringException names a class that matches nothing in the JARsCorrect the name; copy it from the exception or docsStore class names as constants covered by tests
Dependency scoped provided or test onlymvn dependency:tree shows the scope; production lib lacks the JARChange the scope to compile and rebuild the artifactReview scope changes in pull requests like code changes
Class compiled in but its dependency missing at runtimeNoClassDefFoundError instead, naming the absent dependencyShip the missing dependency with the appSmoke-test the packaged artifact, not just the compiled classes
Custom classloader can't see the classSame classpath works in a plain java run but fails in the containerFix loader delegation or move the JAR to the shared libDocument which loaders own which libraries per container
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
LookupDemo.javapublic class LookupDemo {Classpath Lookup at Runtime
DriverLoad.javapublic class DriverLoad {Missing JARs and Dependencies That Cause It
JdbcConnect.javapublic class JdbcConnect {JDBC Driver Loading and the Forgotten JAR
ShowClasspath.javapublic class ShowClasspath {Fixing the Classpath With -cp, Maven, and Gradle

Key takeaways

1
ClassNotFoundException means a runtime lookup by name found nothing on the classpath.
2
Clean compiles don't prevent it; reflective names bypass the compiler entirely.
3
Scope JDBC and library dependencies compile so the JARs actually ship.
4
NoClassDefFoundError is the link-time cousin
compiled fine, missing at runtime.
5
Read the missing class name from the trace, then hunt it with jar tf.
6
Smoke-test the packaged artifact, since tests run on a richer classpath.

Common mistakes to avoid

6 patterns
×

Running java without -cp and expecting it to see your JARs

Symptom
ClassNotFoundException for classes that compile fine, because the runtime classpath contains only the current directory.
Fix
Run with java -cp 'lib/:.' Main and confirm every needed JAR is listed. Prefer lib/ wildcards over naming JARs one by one so added dependencies ride along.
×

Marking the JDBC driver dependency as provided scope

Symptom
Everything passes in tests but the deployed service throws on the driver class, since provided JARs never ship.
Fix
Keep the driver's dependency in compile scope and confirm with mvn dependency:tree. Reserve provided scope for APIs the container truly supplies, like the servlet API.
×

Loading a driver or class by a half-remembered name

Symptom
ClassNotFoundException naming a class that almost matches the real one, with one package segment wrong.
Fix
Read the fully qualified name from the exception and import or load exactly that class. Copy-paste the name instead of typing it from memory.
×

Compiling against a library that never ships to production

Symptom
Clean build, instant crash on boot, and a lib directory missing the JAR the code was compiled with.
Fix
Match the runtime to the build: run mvn dependency:tree or gradle dependencies and diff against the production lib directory. Ship the same versions you compiled against.
×

Treating ClassNotFoundException and NoClassDefFoundError as the same bug

Symptom
Classpath fixes applied to a linkage problem, or dependency hunts for a class whose name was simply misspelled.
Fix
Treat them differently: ClassNotFoundException means fix the classpath or the name; NoClassDefFoundError means fix the deployment so compiled-against classes ship too. Log which one fired before touching config.
×

Building a fat JAR that silently drops a dependency

Symptom
The app runs until the first call into the dropped library, then throws for a class that exists in source and in the build file.
Fix
Shade with relocation, or align all modules on one version of the conflicted library. Inspect the fat JAR with jar tf to confirm which copy won.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What triggers a ClassNotFoundException?
Q02SENIOR
Contrast ClassNotFoundException with NoClassDefFoundError.
Q03SENIOR
Your JDBC connection code throws on the driver class. Walk me through it...
Q04SENIOR
How does the -cp flag interact with CLASSPATH, and what goes wrong?
Q05SENIOR
A service passes all tests but throws on boot in production. What is you...
Q01 of 05JUNIOR

What triggers a ClassNotFoundException?

ANSWER
The JVM searches the classpath for the named class when code calls Class.forName, loadClass, or first touches the class. If no classpath entry contains it, the lookup throws. The message carries the fully qualified name, which tells you exactly what to hunt for across your JARs.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Why does code that compiles still throw at runtime?
02
How do I set the classpath correctly?
03
Do I still need Class.forName for JDBC drivers?
04
How do I tell a missing JAR from a wrong classpath?
05
Why does the distinction from NoClassDefFoundError matter?
06
Can Maven dependency scope cause this in production only?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Exception Handling. Mark it forged?

6 min read · try the examples if you haven't

Previous
Java NullPointerException Fix
8 / 19 · Exception Handling
Next
SLF4J StaticLoggerBinder Warning Fix