A Java stack trace shows where a failure surfaced, which may not be where the bad value originated. If a program crashes while reading an array element, look at how it calculated the index. Start at the reported line, then follow the relevant values back through the calls and assignments that produced them.
Make the bug repeatable first
Debugging gets easier when the same input produces the same failure. Record the input, expected result, actual result, and command or test used to run the program. Keep the full error output, not just its last line. If the bug is intermittent, note what changes between runs: input order, timing, files, environment variables, or shared state.
Before changing code, reduce the problem to the smallest example that still fails. A program that mishandles one name is easier to inspect than an entire import job. Remove unrelated inputs and features one at a time, checking that the failure remains. This minimal reproduction need not be a separate project; it just needs to provide a small, reliable path to the behavior.
Also check that you are running the code you think you are. Save the file, rebuild if needed, and confirm that the run configuration points to the intended class and project. Stale compiled classes, an old terminal process, or a different test configuration can make a valid fix appear ineffective.
- Compile-time error: the compiler cannot produce a valid class, often because of syntax, type, or missing-symbol problems.
- Runtime exception: execution stops on a particular path; the exception and stack trace give you an initial location.
- Incorrect result: execution finishes, but the output violates an expectation. You need to find where the values diverged.
These cases call for different starting points. Fix a compiler error before trying to step through code that cannot run. For an incorrect result, write down a specific expected value rather than deciding only that the output looks wrong.

Read a stack trace as a route through the program
A stack trace names the exception and lists the method calls active when it was thrown. An ArrayIndexOutOfBoundsException reporting index 3, for example, tells you an access fell outside the array’s valid range. The first frame in your own code is usually a good place to start. Library frames can explain the operation, but your code often supplied the input that triggered it.
Read the exception message, then inspect the reported source line and the calls below it in the trace. A frame tells you where execution passed, not why the state became invalid. If a method received a bad argument, check its caller before adding a fix inside the method. If the trace includes a cause, read that too: the outer exception may have been thrown while handling an earlier failure.
Line numbers help only when they match the version you ran. Rebuild after edits, and do not interpret an old trace against changed source. The marked line is evidence, not proof that the defect began there.
Use a small failing example to form a hypothesis
Suppose a method is meant to sum positive integers in an array:
static int sumPositives(int[] values) {
int total = 0;
for (int value : values) {
if (value >= 0) {
total += value;
}
}
return total;
}
The condition includes zero, which conflicts with “positive.” But adding zero cannot change the numeric sum, so that line would not explain a wrong sum for an input containing zero. Suspicious code is not automatically the cause of the failure you observed. To make the distinction visible, consider a method that counts positive values instead:
static int countPositives(int[] values) {
int count = 0;
for (int value : values) {
if (value >= 0) {
count++;
}
}
return count;
}
For {-2, 0, 4}, the expected count is 1, but the method returns 2. Now there is a testable hypothesis: >= 0 includes zero. Changing it to > 0 should make that input pass. Test an all-zero array and an array of positive numbers as well, so you can check that the fix follows the intended rule rather than correcting just one result.
That is the debugging loop: observe a discrepancy, propose an explanation, check it against the program’s state or a small test, and edit only when the evidence supports the change. If the hypothesis fails, keep the observation and drop the guess.
Step through code with an IDE debugger
Most Java IDEs let you pause execution at a breakpoint. Set one shortly before the incorrect result, start the program in debug mode, and inspect the local variables when it pauses. In the counting method, a breakpoint on the if line lets you watch value and count on each iteration. When value is zero, the condition evaluates to true and the count increases.
Choose the right stepping command
- Step over executes the current line and stops at the next line in the same method. Use it when a called method is already trusted or irrelevant.
- Step into enters a method call so you can inspect its implementation.
- Step out runs until the current method returns and pauses in its caller.
- Resume continues to the next breakpoint or until the program finishes.
There is little value in stepping through a large application from its entry point. Put a breakpoint near the first known difference between expected and actual behavior. If the discrepancy appears after a loop, pause before and after it, compare state, then move the breakpoint inside. You can narrow the search without following unrelated initialization code.
A conditional breakpoint pauses only when an expression is true. A condition such as value == 0 avoids stopping for every element in this example. A watch expression can show a value as execution moves, but be careful what you evaluate: calling a method that changes state can alter the program you are investigating.
Debugger displays also need interpretation. An object variable may hold a reference while its fields contain the state you need; a collection’s size may tell you more than its identity. And stepping changes timing. A thread-related bug may disappear or move when execution pauses, making targeted logs and repeated tests more useful than breakpoints alone.

Print and log values without hiding the failure
Temporary print statements help when a debugger is unavailable or the failure takes many iterations to appear. Print a value with enough context to identify it: System.out.println("index=" + index + ", size=" + items.size()); tells you more than printing index alone. Put the output immediately before the suspected branch or operation, then compare it with the expected state.
For a longer-running application, use its existing logging facility instead of filling normal output with ad hoc messages. Include a meaningful event description and relevant identifiers, and choose a level that fits the event. Preserve the original exception when reporting a failure; replacing it with a generic message discards useful evidence. If you catch an exception, handle it deliberately rather than printing a line and continuing with invalid state.
Logs can expose sensitive information. Do not print passwords, tokens, private keys, or complete personal records, even in a local lab whose output might later be shared. Use harmless test data and redact values before copying logs into a bug report. Remove temporary prints after the fix, but keep logging that helps operate the application.
Turn the failure into a regression test
A reproducible bug deserves a test that fails before the fix and passes afterward. A unit test for countPositives can assert that {-2, 0, 4} returns 1. Run it before editing to confirm that it captures the reported behavior. If it already passes, the bug is elsewhere or the test misses the failing path.
Test boundaries, not just ordinary inputs. For counting positives, try an empty array, only negative numbers, zero alone, and several positive numbers. For indexing, check an empty collection and the first and last valid positions. For text handling, consider empty input and unexpected whitespace. Base each expected result on the method’s documented contract; do not invent an expectation just to make a test pass.
When a bug spans methods, a focused integration test may be more useful than an isolated helper test. Keep the input small and the assertion precise. Checking only that no exception was thrown will miss a method that quietly returns the wrong answer. Once the targeted test passes, run the broader suite to catch effects on other behavior.
Common traps that waste debugging time
Changing several things at once
If you edit the condition, reorganize the loop, and change input validation together, a passing test will not tell you which change mattered. Make the smallest plausible correction first. Keep cleanup separate from the bug fix when you can, so cause and effect remain visible in the diff.
Assuming a null value appeared at the crash site
A NullPointerException identifies an attempted use of a null reference. Trace where that reference was assigned or returned. A null check at the crash site may stop the exception while leaving the underlying mistake untouched. Decide whether null is valid input or a broken assumption, then handle it at the boundary where that decision belongs.
Ignoring external state
A missing file, unexpected character encoding, environment setting, or test data left from an earlier run can change a program’s behavior. Verify those inputs before rewriting an algorithm. If the problem occurs on only one machine, compare the runtime version, launch arguments, and relevant configuration—not just the source files.
Confusing correlation with cause
The last message printed before a crash may not come from the failing operation. Output can be buffered, and another thread may have printed it. Follow the stack trace and reproduce the failure with controlled input. For concurrent code, include a thread name in diagnostic logs and check for shared mutable state rather than treating the most recent message as the cause.
Keep a short debugging record
For a stubborn bug, record each testable hypothesis and its outcome. “Zero increments the count” is useful; “the loop is broken” is not. Note the exact input, observed output, and relevant breakpoint or log finding. You will avoid repeating checks and have a clearer explanation of the eventual fix.
After changing value >= 0 to value > 0, rerun the test for {-2, 0, 4}, then run the boundary tests and full suite. Check the final diff for leftover print statements and unrelated edits. Leave the failing input in the test suite: if a future change counts zero again, that test should make the mistake easy to spot.
