A painful truth: Your code can compile, run, and still be completely incorrect.
The good news? There are reliable ways to catch these sneaky bugs long before they cause trouble in production.
In my 10+ years of reviewing Java code, I've seen one pattern again and again: Developers who use the techniques below consistently ship fewer bugs — and spend far less time debugging production issues.
Let's explore these practices in the simplest possible way, supported by real-world examples.

1. Write Unit Tests That Actually Test Behavior, Not Just Lines
Unit tests exist to prove your code does what you think it does, not just to increase coverage.
Example: Poor Unit Test
@Test
void testDiscount() {
Discount d = new Discount();
d.apply(100);
}This test "covers" the line but tests absolutely nothing.
Example: Useful Unit Test
@Test
void shouldApplyTenPercentDiscount() {
Discount d = new Discount();
double result = d.apply(100);
assertEquals(90, result);
}Why this catches wrong-but-compiling code
Because behavior is validated, not just execution.
Better tests → fewer logical misunderstandings.
2. Use Defensive Programming
A lot of real bugs come from bad assumptions about what input your code will receive.
Example: Without defensive programming
double calculateAverage(List<Integer> scores) {
return scores.stream().mapToInt(i -> i).average().getAsDouble();
}If scores is empty → RuntimeException
If scores is null → NullPointerException
Example: With defensive programming
double calculateAverage(List<Integer> scores) {
if (scores == null || scores.isEmpty()) {
return 0; // or throw custom exception
}
return scores.stream().mapToInt(i -> i).average().orElse(0);
}Why this helps
Your code becomes predictable, not "surprises waiting to happen."
3. Think in Terms of Contracts
A "contract" is a simple idea:
What your method guarantees, and what your method expects.
Example contract:
Method: findUserById(String id)
- Expects:
idis not null - Guarantees: returns either a user or an empty optional
- Never throws due to missing user
Why this matters
Because many wrong-but-compiling errors come from unclear expectations.
Once you define contracts, the code becomes cleaner, safer, and easier to test.
4. Use Assertions to Catch Impossible States Early
Assertions help verify assumptions that "should never be wrong."
Example
public void process(Order order) {
assert order != null : "Order must never be null here!";
...
}Assertions are disabled in production by default, but they are lifesavers in development and testing.
Why this helps
It catches logical errors in your assumptions before they spread.
5. Rely on Static Analysis Tools
Tools like:
- IntelliJ inspections
- SonarLint
- SpotBugs
- Checkstyle
- PMD
These tools catch things like:
- unused variables
- suspicious expressions (
==instead of.equals) - nullability issues
- redundant conditions
- unreachable code
- potential memory leaks
Example: Static analysis catch
You write:
if (value = true) { ... }This compiles because value = true is an assignment.
Static analysis tools will scream immediately.
Why this helps
You get automatic reasoning from tools that specialize in finding logical inconsistencies you might miss.
6. Do Meaningful Code Reviews
Code reviews are not about judging the developer — they are about catching assumptions.
What a shallow review looks like:
- "LGTM"
- "Rename variable"
- "Add comments"
What a meaningful review looks like:
- "What happens if this list is empty?"
- "Why are we assuming
HashMaporder?" - "Could this cause a race condition?"
- "Does this match the business rule?"
- "What if
nullis passed here?"
Why this matters
Fresh eyes reveal hidden assumptions — especially assumptions you never questioned.
7. Log Smartly
Logging is not for decoration. Good logs help you spot issues before users do.
Bad logging
logger.info("Starting process...");Smart logging
logger.debug("Processing order id={}, items={}", orderId, itemCount);Why this helps
If something goes wrong, logs reveal the path your code took — even when the code "worked."
8. Test Edge Cases
Most production bugs come from scenarios nobody tested:
- empty list
- null input
- extreme values
- long strings
- negative numbers
- concurrency
- boundary indexes
- API failures
Example: Off-by-one caught by testing boundaries
assertEquals("A", substring("ABC", 0, 1));
assertEquals("C", substring("ABC", 2, 3)); // catches index logic errors9. Ask the Golden Question: "What Could Break Here?"
Whenever you finish writing code, ask this one question:
"If this fails in production, what will be the most likely reason?"
This question alone catches:
- hidden assumptions
- missing checks
- fragile conditions
- misunderstood APIs
- unhandled edge cases
It is a simple habit that dramatically raises code correctness.
The Real Secret
Wrong-but-compiling code happens when developers rely on:
- intuition
- assumptions
- "it worked on my machine"
- hopeful thinking
Correct code comes from:
- tests
- checks
- contracts
- tools
- reviews
- verification
To be blunt:
If you don't verify your code, your users will. And their verification is called "bugs."
Final Thoughts
Good code doesn't happen by accident. It's the result of small habits repeated consistently.
If you start writing defensive code, testing assumptions, reviewing behavior, and letting tools help you — you'll prevent 90% of the wrong-but-compiling issues that frustrate developers every day.
In software, correctness isn't the absence of errors. It's the presence of intentional checks.