Could Not Find or Load Main Class: Java Fix
Run java with the class name, not Foo.class, from the classpath root with -cp .
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Java installed with javac and java on your PATH
- ✓A small project with one packaged class to experiment on
- ✓Basic comfort with cd, ls, and running commands in a terminal
- You're launching from the wrong directory or naming the file instead of the class: run java Foo from the classpath root, never java Foo.class.
- A packaged class like com.shop.Main must live at com/shop/Main.class under some root, and you must launch it by its full name from that root.
- The -cp flag replaces CLASSPATH for one launch, so java -cp .:lib/* com.shop.Main beats any stale environment variable.
- JAR launches need a Main-Class line in META-INF/MANIFEST.MF; check it with unzip -p and rebuild with jar cfe when it's missing.
Picture a mailroom that delivers letters by department and name, like Sales/Ana. You must hand the letter in at the front desk with the full address. If you walk into the Sales room and ask for Ana, the clerk can't help — you're standing in the wrong spot. If you write Sales/Ana.class on the envelope, there's no such person. That's this error: your class exists, but you asked from the wrong directory or with the wrong name, so the JVM's mailroom reports it missing.
You've just compiled cleanly, the .class file is sitting right there, and java still tells you it can't find your main class. It's maddening because the message sounds like your code is missing when the real problem is almost always where you stand, what you typed, or what you told the JVM to search. This error is the launcher's way of saying the lookup failed — not that your program is broken.
The confusion runs deep because five different mistakes produce the identical message. Standing inside the package directory instead of above it. Typing java Foo.class instead of java Foo. A package line that doesn't match the folders. A CLASSPATH variable that points somewhere stale. A JAR whose manifest never named a main class. Each one needs a different fix, so guessing wastes an afternoon.
IDEs hide the whole mechanism. They build the classpath silently, so code that runs with the green arrow dies in a terminal. That's not a code bug — it's a classpath you never had to think about.
This guide walks through all six angles: classpath roots versus the current directory, package-to-folder mapping, naming the class instead of the file, -cp versus CLASSPATH, JAR manifests, and the IDE-to-CLI gap. You'll get a production incident where a bad start script faked a broken build, a debug guide with exact commands, and comparisons that match each symptom to its fix.
The Classpath Root Versus the Current Directory
The JVM doesn't search your current directory for the class file — it searches each classpath root for the package path beneath it. When you run java -cp . com.shop.Main, the dot means this directory is a root, and the launcher looks for ./com/shop/Main.class. If you're standing inside com/shop already, that lookup becomes ./com/shop/com/shop/Main.class, which obviously doesn't exist.
Think of the root as the place where the package tree starts. One project can have several roots: the current directory, an output folder like out/, and every JAR on the classpath. The launcher checks each root in order and loads the first match. That's why java -cp out com.shop.Main works from anywhere — the root travels with the flag instead of depending on your shell's location.
The default classpath when you pass no -cp is just the current directory. That's the trap: developers compile into out/ but launch from the project root, or launch from inside the package folder, and in both cases the default root points at the wrong place. You'll stare at Main.class in ls output while the JVM insists it can't see it.
The fix is boring on purpose. Decide on one root (usually your build output dir), cd there or name it in -cp, and always launch with the fully qualified name. Start scripts should do both explicitly: cd to a fixed directory first, then pass -cp. That removes the shell's location from the equation entirely, which is exactly what you want on a server you'll never log into by hand.
Package Names Must Match the Folder Tree
A package declaration is a promise about folders. Writing package com.shop; means the compiled Main.class must live at com/shop/Main.class relative to some classpath root. The compiler won't force the source file to sit in matching folders, so you can break this promise without any error — javac happily emits Main.class wherever you tell it, and the failure waits for launch day.
The classic breakage is compiling without -d. Running javac src/com/shop/Main.java drops Main.class next to your shell or beside the source, with no com/shop tree around it. The class file is valid, the code is correct, and no root on earth can resolve com.shop.Main from it, because the required path fragments simply aren't there.
javac -d exists to prevent exactly this. The -d flag names the root, and the compiler creates the package folders beneath it automatically. javac -d out src/com/shop/Main.java always yields out/com/shop/Main.class regardless of where your shell stands. Make -d non-negotiable in every build script and Makefile and this whole category of failure disappears.
When you're already broken, confirm with find: find . -name Main.class should show a path ending in com/shop/Main.class. If it shows Main.class sitting bare in some folder, the tree is wrong — recompile with -d and relaunch from the root you gave -d. Don't shuffle .class files by hand; you'll fix one launch and break the next.
java Foo.class Versus java Foo: Name the Class, Not the File
The java command wants a class name, full stop. java Foo means find class Foo on the classpath and run its main method. java Foo.class means find package Foo, class class — which doesn't exist — and the launcher reports it exactly like a missing class. The file suffix belongs to javac and the filesystem; it must never appear after java.
This bites in two directions. Newcomers type the filename they see in ls, and veterans copy-paste a path from an error log. Both get the same confusing message, and both then chase classpath settings that were never the problem. If your failing command ends in .class or .java, stop touching the classpath — the command itself is wrong.
There's a subtler sibling: the class loads but has no valid main. A missing static, a non-public modifier, or a wrong parameter type all compile cleanly, then fail at launch with a main-method variant of this error. The message says it can't find or load the main class, which reads like a classpath problem but is really a signature problem. Check that main is exactly public static void main(String[] args).
Build the habit of launching from a script or an alias instead of typing the command fresh each time. Scripts don't develop typos under deadline pressure, and a smoke test that runs the script catches both the suffix mistake and the signature mistake before your users do. Your future self at 2 AM will be grateful.
-cp and CLASSPATH: The Flag Wins Every Time
CLASSPATH is a global variable with local consequences. It points somewhere sensible on your laptop and somewhere stale — or nowhere — on the server, in CI, and in containers. Code that launches fine where CLASSPATH happens to be right will fail everywhere else with this error, and nobody will think to blame an environment variable they didn't know existed.
The -cp flag (or -classpath, same thing) replaces CLASSPATH completely for one launch. That's what makes it the fix: java -cp .:lib/* com.shop.Main searches the current directory plus every JAR in lib/, regardless of what the environment claims. When the flagged version works and the bare version doesn't, you've proven the variable was the problem.
Two details trip people up. First, the lib/ wildcard only matches .jar files directly inside lib/, not in subfolders, and it must be quoted (or at least protected) so your shell doesn't expand it before the JVM sees it. An unquoted lib/ in an empty directory can vanish into nothing. Second, separators differ: colon on Linux and macOS, semicolon on Windows. A copied Linux command fails on Windows for that reason alone.
The production rule is simple: never rely on CLASSPATH in anything that ships. Set -cp on every java invocation in scripts, Dockerfiles, and systemd units. If you must keep the variable for interactive use, treat a bare java launch as a convenience and the flagged launch as the truth — and debug against the flagged one.
JAR Manifests: When java -jar Can't Find Main-Class
java -jar doesn't take a class name at all — it asks the JAR's manifest which class to run. That answer lives in META-INF/MANIFEST.MF as a Main-Class: com.shop.Main line. Build tools sometimes produce JARs without it (plain library packaging, a hand-rolled jar cf, a shade misconfiguration), and then the launcher reports it can't find the main class even though the class bytes sit right inside the archive.
Diagnosing this takes two commands. jar tf shop.jar | grep Main proves the class is present; unzip -p shop.jar META-INF/MANIFEST.MF shows what the manifest claims. When the class is there but the Main-Class line isn't, you've found it — no classpath archaeology needed. This split saves hours because it separates a packaging bug from a lookup bug in seconds.
The fastest repair is jar cfe, whose e flag writes the entry point for you: jar cfe shop.jar com.shop.Main -C out . packages everything under out/ and stamps the manifest. For the long term, declare it in your build — Maven's maven-jar-plugin archive/manifest block or Gradle's manifest { attributes 'Main-Class': ... } — so every artifact leaves the pipeline runnable. Then verify in CI by reading the manifest back, not by assuming the config worked.
One more trap: java -jar ignores -cp and CLASSPATH entirely. Dependencies must ride in the manifest's Class-Path entry or inside the JAR itself as a fat JAR. If your -jar launch suddenly can't see lib/, that's why — the flag you added is being silently ignored by design.
IDE Versus CLI: Why the Green Arrow Works and the Terminal Doesn't
IDEs are wonderful liars about classpaths. IntelliJ, Eclipse, and VS Code assemble a launch classpath from your module settings, dependency lists, and output folders, then run java with a -cp flag so long you'd never type it. Your code works because the tool did the lookup work for you invisibly. The moment you step outside — a terminal, a Docker build, a teammate's machine — that invisible classpath vanishes and this error appears on code that never changed.
The tell is the long command line. Most IDEs can show the exact java invocation they used (IntelliJ prints it atop the run console; Eclipse shows it in debug configs). Copy that -cp value and you'll see output directories and dozens of JARs you'd never have listed by hand. Recreate the essentials of it in your script and the CLI failure usually clears immediately.
Output folders are the other half of the gap. IDEs compile to out/ or target/ and point the runtime there; a hand-rolled javac without -d scatters .class files beside sources. Same sources, different layouts, different results. Align them by building with your real build tool (Maven, Gradle) and launching against its output dir, not by copying .class files around until it works.
Treat the IDE as a development convenience and the script as the contract. If it doesn't launch from the command line with an explicit -cp, it isn't really runnable — it's just IDE-runnable. Make the pipeline, not the green arrow, the definition of working.
The Start Script That Hid Eight Servers' Worth of Classes
- A start script is production code — review its cd and -cp lines as carefully as application code, because they decide whether the JVM can see your classes at all.
- Smoke-test the launch itself, not just the build. A pipeline that compiles but never runs the start command will happily ship a script that points at nothing.
- Log the effective launch: echo the working directory and full java command at startup so the next incident starts with evidence instead of guesses.
| File | Command / Code | Purpose |
|---|---|---|
| classpath-root-demo.sh | find out -name "*.class" | The Classpath Root Versus the Current Directory |
| src | public class Main { | Package Names Must Match the Folder Tree |
| src | public class Main { | java Foo.class Versus java Foo |
| classpath-flag-demo.sh | echo "CLASSPATH=$CLASSPATH" | -cp and CLASSPATH |
| manifest-fix-demo.sh | jar tf shop.jar | grep -E "Main|MANIFEST" | JAR Manifests |
Key takeaways
Common mistakes to avoid
5 patternsTyping java Foo.class instead of java Foo
Running java from inside the package folder
Depending on a stale CLASSPATH environment variable
Compiling without javac -d so class files land in the wrong place
Shipping a JAR with no Main-Class and running java -jar
Interview Questions on This Topic
Why does java Foo.class fail with this error?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Exception Handling. Mark it forged?
6 min read · try the examples if you haven't