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.

None

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: id is 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 HashMap order?"
  • "Could this cause a race condition?"
  • "Does this match the business rule?"
  • "What if null is 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 errors

9. 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.