Debugging investigates why a Java program behaves differently from its requirement. Logging records selected events so you can understand execution later, particularly when the original debugger session is unavailable. The tools complement one another: a breakpoint reveals live state, while a well-placed log describes a decision or failure with useful context. This lesson uses the JDK's java.util.logging API so the examples require no additional library, and focuses on messages that actually help explain program behavior.
Reproduce one concrete symptom
Start with the input, expected result, and actual result. Reduce the failing case without changing the behavior you are investigating. For an incorrect invoice total, record the quantity, unit price, and rule for discounts. A vague report that the program is broken provides no comparison point. Once the case is reproducible, inspect the earliest intermediate value that diverges from the expectation instead of adding unrelated fixes around the final print statement.
Use debugger state rather than guesses
A breakpoint pauses at a chosen statement so you can inspect local variables, fields, and the call stack. Step over executes a call without entering it; step into follows the called method; step out returns to the caller. Watch expressions help compare a calculation with its operands. Remember that evaluating an expression can call methods and change state, so avoid destructive expressions in the debugger when diagnosing a delicate failure.
Choose logging levels by meaning
java.util.logging provides levels including SEVERE, WARNING, INFO, FINE, FINER, and FINEST. INFO is suitable for meaningful ordinary milestones, WARNING for recoverable unexpected conditions, and SEVERE for serious failure. Detailed diagnostic values belong at a lower level rather than overwhelming normal output. Effective logging also depends on handler levels: enabling a logger alone may not display fine-level records if the configured handler still filters them.
Keep useful context without leaking secrets
A message such as invalid quantity for order A14 is more actionable than error happened. Include a stable identifier and the relevant decision, but avoid passwords, authentication tokens, payment details, and unnecessary personal information. Log an exception with the logging API's throwable argument when a stack trace is useful. Do not replace it with only getMessage, which may lose the location and causal chain needed to diagnose the failure.
Avoid expensive disabled messages
A Supplier<String> logging overload constructs a message lazily when the level is enabled. This avoids unnecessary formatting work for disabled diagnostic messages. It does not make expensive computation inside business code disappear; only the supplied message expression is deferred. Use logging for observation rather than controlling correctness. The program should produce the same domain result whether a diagnostic level is enabled or not.
Control configuration and output expectations
Production logging normally includes timestamps, levels, and destinations configured outside the domain calculation. Exact default console formatting can vary and often writes to standard error. For reproducible examples below, a custom Handler prints only message text. This isolates the API behavior being taught from timestamp differences. Larger applications often use a logging facade and configurable backend, but the same principles of context, level choice, and error ownership still apply.
Practice tasks and checks
Introduce an incorrect discount rule in a small invoice method and create one input where it fails. Set a breakpoint before the conditional and inspect the subtotal, threshold, and selected branch. Fix the rule only after you can explain the wrong choice. Add an INFO message for accepted orders and a WARNING for rejected quantity text. Then configure the logger to WARNING and confirm the calculation result remains unchanged even though routine messages disappear. Write a test for the original failing input so the debugger investigation leaves a lasting regression check. Finally, review each logged field: remove passwords or unnecessary personal data, retain a useful operation identifier, and attach the original exception where the handling boundary needs its diagnostic detail.
Java Summary
- Begin debugging with a reproducible input and expected result.
- Logging should describe meaningful events with safe, relevant context.
- Preserve exception causes at the boundary responsible for reporting failures.
- Continue with debugger workflows when these examples are clear.
Java Examples
Each example shows the code, the output it produces and what each part does.
Record a decision with useful context
Save this program as OrderLog.java. Compile it with javac OrderLog.java and run it with java OrderLog.
Expected output:
The logger is named so related events can share configuration. Disabling parent handlers prevents duplicate default-console output in this demonstration. The custom handler prints the message from each accepted LogRecord, omitting timestamps. The message identifies the order and quantity, making it more useful than an isolated accepted string. It deliberately includes no customer details or secrets.
import java.util.logging.*;
public class OrderLog {
static Logger logger() {
Logger log = Logger.getLogger("order-demo");
log.setUseParentHandlers(false);
log.addHandler(new Handler() {
public void publish(LogRecord record) { System.out.println(record.getMessage()); }
public void flush() {}
public void close() {}
});
return log;
}
public static void main(String[] args) {
Logger log = logger();
int quantity = 3;
log.info("Order A14 accepted: quantity=" + quantity);
}
}Order A14 accepted: quantity=3Show that disabled messages stay lazy
Save this program as LazyLogMessage.java. Compile it with javac LazyLogMessage.java and run it with java LazyLogMessage.
Expected output:
The logger's level rejects INFO, so it does not invoke the message supplier. Message constructed is therefore absent. The ordinary print afterward still executes, showing that diagnostic configuration does not stop application execution. If you changed the level to INFO, the supplier would run, but default handler formatting would then be environment-dependent; this example tests only the disabled-message path.
import java.util.logging.Level;
import java.util.logging.Logger;
public class LazyLogMessage {
public static void main(String[] args) {
Logger log = Logger.getLogger("lazy-demo");
log.setLevel(Level.WARNING);
log.info(() -> {
System.out.println("Message constructed");
return "Detailed state";
});
System.out.println("Calculation continues");
}
}Calculation continuesPreserve the failing input and cause
Save this program as ParseDiagnostic.java. Compile it with javac ParseDiagnostic.java and run it with java ParseDiagnostic.
Expected output:
The parser throws because three is not numeric input. The log call stores both a contextual message and the original Throwable in the LogRecord. Our handler prints the type to keep output deterministic; a production formatter could print the complete stack trace. Logging the error does not itself choose a retry or fallback. The application must separately decide what response to give the person or caller that supplied the invalid quantity.
import java.util.logging.*;
public class ParseDiagnostic {
public static void main(String[] args) {
Logger log = Logger.getLogger("parse-demo");
log.setUseParentHandlers(false);
log.addHandler(new Handler() {
public void publish(LogRecord record) {
System.out.println(record.getMessage());
System.out.println(record.getThrown().getClass().getSimpleName());
}
public void flush() {}
public void close() {}
});
try {
Integer.parseInt("three");
} catch (NumberFormatException error) {
log.log(Level.WARNING, "Quantity text rejected", error);
}
}
}Quantity text rejected
NumberFormatExceptionJava FAQ
Should I log an exception at every layer?
Usually log where a failure is handled or reported meaningfully. Repeatedly logging and rethrowing the same exception can create duplicate records that look like several failures. Preserve the cause while it moves toward the responsible boundary.
Can logs replace tests?
No. Logs provide observations; tests assert expected behavior. A useful diagnostic message helps investigate a failure, while a regression test confirms that a corrected case stays correct after later changes.
Which logging library should I use in Java?
java.util.logging is built into the JDK and fine for small programs. Most applications log through SLF4J, a common interface, with Logback or Log4j 2 doing the actual writing. Spring Boot sets this up by default. Whichever you choose, use one consistently across the project.
Is System.out.println good enough for debugging?
For a quick check while learning, yes. In real programs, print statements are hard to switch off, have no levels or timestamps and get forgotten in the code. A debugger answers "what is the value right now?" better, and logging answers "what happened?" in a form you can configure and keep.
What should never be written to a log?
Passwords, API keys, session tokens, full card numbers and sensitive personal data. Logs are often copied to other systems and kept for a long time, so a leaked secret there is hard to remove. Log an identifier, such as an order number or a masked email address, instead.
Java Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What makes an order log useful?
Why is Message constructed absent?
Java Quiz
2 questions to check what you learned.
What preserves an exception for logging?
- Passing the Throwable to log
- Logging only getMessage
- Discarding the cause
What should come first when debugging?
- A reproducible input and expected result
- Renaming unrelated classes
- Turning every log into SEVERE