Back to Blog Home

A Guide to Logging and Debugging in Java

-

Why Logging and Debugging Matter in Java

During the development of your program or scripts, you might rely on simple println() statements to trace program execution flows and identify issues in your code. But as projects grow in size and complexity, print statements quickly become messy. A better approach to tracing program execution is logging, an approach that provides a consistent and organized way to track your application’s behavior, allowing you to systematically identify and resolve issues.

In this guide, we’ll explore how to set up logging in Java, use essential debugging tools effectively, and apply best practices to create stable and maintainable applications.

We’ll also look at how Sentry can help you monitor and manage errors in your Java applications. By understanding the fundamentals and advanced techniques, you’ll be equipped to tackle issues confidently within your Java projects.

Setting Up Logging in Java

You may have used System.out.println() to log and debug your code, for example:

System.out.println()Click to Copy
public class Main { public static void main(String[] args) { System.out.println("Debugging: Starting the program"); int result = 5 + 3; System.out.println("Debugging: Result is " + result); } }

Would output:

Debugging: Starting the program
Debugging: Result is 8

This kind of console logging can be helpful for small projects or quick tests, but it has many limitations. Unsaved console logs disappear when the program stops, making it difficult to keep a history of what happened. They also don’t allow you to categorize messages by severity, which makes it hard to spot critical issues in the output.

Basic logging in Java can be achieved using the built-in java.util.logging package. This built-in framework allows you to easily record and output different events in your application, such as informational messages, warnings, or errors.

Using java.util.logging, you can specify log levels to indicate the importance of each message:

  • SEVERE: Indicates a severe error that may prevent the application from functioning correctly.

  • WARNING: Indicates a potential issue that may require attention.

  • INFO: Provides general information about the application’s execution progress.

  • CONFIG: Indicates configuration-related messages.

  • FINE, FINER, FINEST: Used for debugging purposes to provide detailed information.

You can use these log levels with the following syntax: logger.<LEVEL>("<YOUR_MESSAGE_HERE>").

You may need to add the following to your module-info.java file to allow logging:

requires java.logging;

Here’s how you can set up a logger and log messages with different levels:

import java.util.logging.Logger;Click to Copy
import java.util.logging.Logger; public class LoggingLevelsExample { private static final Logger logger = Logger.getLogger(LoggingLevelsExample.class.getName()); public static void main(String[] args) { logger.fine("This is an info message"); logger.config("This is a configuration message"); logger.info("This is an info message"); logger.warning("This is a warning message"); logger.severe("This is a severe error message"); } }

This outputs:

Output of the logging example

Notice how the log messages are timestamped and include the class name and method where the log statement was called. This information can be invaluable when tracking down bugs in complex applications.

Setting the Log Level in Java

In the example above you’ll notice that only the INFO, WARNING, and SEVERE messages are logged, while the CONFIG and FINE messages are not.

This is because the default log level is set to INFO, which means only messages of INFO level and above will be recorded.

Log level hierarchy:

  • SEVERE

  • WARNING

  • INFO Only this level and above are logged by default

  • CONFIG

  • FINE

  • FINER

  • FINEST

You can change the log level to control which messages are recorded. For example, to log messages of FINE level and above, you can use a ConsoleHandler to set the log level to FINE:

ConsoleHandler to set the log level to FINE:Click to Copy
import java.util.logging.Logger; import java.util.logging.Level; import java.util.logging.ConsoleHandler; public class LoggingLevelsExample { private static final Logger logger = Logger.getLogger(LoggingLevelsExample.class.getName()); public static void main(String[] args) { logger.setLevel(Level.FINE); logger.setUseParentHandlers(false); ConsoleHandler handler = new ConsoleHandler(); handler.setLevel(Level.FINE); // We need to create a new ConsoleHandler to override the default level logger.addHandler(handler); logger.fine("This is a debug message"); logger.config("This is a configuration message"); logger.info("This is an info message"); logger.warning("This is a warning message"); logger.severe("This is a severe error message"); } }

Now the output will include the FINE and CONFIG messages:

Output of the logging example with FINE level


How to Log to a File in Java

You can also save logs to a file, which allows you to keep a persistent record of your application’s behavior for future analysis. Saving logs to a file makes it easier to track down issues after the application has stopped running or to compare logs from different runs to identify patterns across different configurations.

Let’s extend the previous example to save the log messages to a file. We’ll use the FileHandler class to write logs to a file called application.log:

FileHandler class to write logsClick to Copy
import java.util.logging.Logger; import java.util.logging.FileHandler; import java.util.logging.SimpleFormatter; public class FileLoggingExample { private static final Logger logger = Logger.getLogger(FileLoggingExample.class.getName()); public static void main(String[] args) { try { FileHandler fileHandler = new FileHandler("application.log", true); fileHandler.setFormatter(new SimpleFormatter()); logger.addHandler(fileHandler); } catch (Exception e) { logger.severe("Failed to set up file handler for logging"); } logger.info("This is an info message"); logger.warning("This is a warning message"); logger.severe("This is a severe error message"); } }

Running this code saves the log messages to a file named application.log in the project directory. You can then open this file to review the logs:

Screenshot of the log file content

Logging Variables and Exceptions in Java
When debugging, it’s often helpful to log the values of variables or exceptions to understand the state of the application at a particular point in time.

Suppose we have a method that divides two numbers, and we want to log the values of the numbers being divided:

example: Logging the values of the numbers being divided Click to Copy
import java.util.logging.Logger; public class LoggingVariablesExample { private static final Logger logger = Logger.getLogger(LoggingVariablesExample.class.getName()); public static void main(String[] args) { int a = 50; int b = 10; logger.info("Dividing " + a + " by " + b); try { int result = divide(a, b); logger.info("Result: " + result); } catch (ArithmeticException e) { logger.severe("Error: " + e.getMessage()); } } public static int divide(int x, int y) { return x / y; } }

This will output:

2024-11-18 14:30:00 INFO LoggingVariablesExample: Dividing 50 by 10
2024-11-18 14:30:00 INFO LoggingVariablesExample: Result: 5

Now, say we want to log an exception that occurs when dividing by zero, and we want to see the stack trace. We’ll change the b variable’s value to 0:

stack trace for logging exceptionClick to Copy
import java.util.logging.Logger; import java.util.logging.Level; public class LoggingVariablesExample { private static final Logger logger = Logger.getLogger(LoggingVariablesExample.class.getName()); public static void main(String[] args) { int a = 50; int b = 0; logger.info("Dividing " + a + " by " + b); try { int result = divide(a, b); logger.info("Result: " + result); } catch (ArithmeticException e) { logger.severe("Error: " + e.getMessage()); logger.log(Level.SEVERE, "Exception occurred", e); } } public static int divide(int x, int y) { return x / y; } }

This will output:

Logging output for java exampleClick to Copy
Nov 21, 2024 9:48:49 PM LoggingVariablesExample main INFO: Dividing 50 by 0 Nov 21, 2024 9:48:49 PM LoggingVariablesExample main SEVERE: Error: / by zero Nov 21, 2024 9:48:49 PM LoggingVariablesExample main SEVERE: Exception occurred java.lang.ArithmeticException: / by zero at LoggingVariablesExample.divide(LoggingVariablesExample.java:23) at LoggingVariablesExample.main(LoggingVariablesExample.java:14)

Notice how the stack trace is included in the log message, which lets us know exactly where the exception occurred.

Four Best Practices for Logging

Here are some best practices to keep in mind:

  • Set the optimal logging level: Logs are helpful only when you can use them to track down important errors that need to be fixed. Depending on the specific application, be sure to set the optimal logging level. Logging too many events can be suboptimal from a debugging viewpoint, as it’s difficult to filter through the logs to identify errors that require immediate attention.

  • Avoid logging sensitive information: Be cautious about logging sensitive information such as passwords, API keys, or personal data.

  • Rotate the log files to facilitate easier debugging: Log files for extensive applications with several modules are likely to be large. Rotating log files periodically can make it easier to debug such applications. Rotate log files by setting a maximum file size or time interval after which a new log file is created. This way, you can easily locate issues by looking at the most recent log files.

For example, you can set up a FileHandler with the following syntax:

file handler in javaClick to Copy
new FileHandler("<filename>.log", <maxFileSize>, <numFiles>)

This will create a new log file when the current file reaches the specified size, up to the maximum number of files specified.

file handler 2Click to Copy
FileHandler fileHandler = new FileHandler("application.log", 1024 * 1024, 10, true); // 1MB file size, 10 files
  • Use log formatting to make logs more readable: Log formatting can help make your logs more readable and consistent. You can use a Formatter to customize the format of your log messages. For example, you can include the timestamp, log level, class name, and message in a specific format.

    java formatterClick to Copy
    SimpleFormatter formatter = new SimpleFormatter() { private static final String format = "[%1$tF %1$tT] [%2$-7s] %3$s %n"; @Override public synchronized String format(LogRecord lr) { return String.format(format, new Date(lr.getMillis()), lr.getLevel().getLocalizedName(), lr.getMessage() ); } }; fileHandler.setFormatter(formatter);

    This will format the log messages with a timestamp down to the second, the log level, and the message:'

formatted java logsClick to Copy
[2024-11-18 14:30:00] [INFO   ] This is an info message [2024-11-18 14:30:00] [WARNING] This is a warning message [2024-11-18 14:30:00] [SEVERE ] This is a severe error message

Java Debugging 101: How to Debug in Eclipse

Debugging involves tools and techniques that help identify and fix issues in your code. Understanding how to effectively use breakpoints, inspect variables, and step through the code is key to debugging faster.

Let’s debug our division example in Eclipse. Eclipse is one of the most popular Java IDEs and offers powerful debugging features that make it easier to find and resolve bugs.

We’ll be debugging the following code, which has an error where b is set to x - x rather than x + x:

public class DebugExampleClick to Copy
public class DebugExample { public static void main(String[] args) { int a = 50; int x = 5; int b = x - x; int result = divide(a, b); System.out.println("Result: " + result); } public static int divide(int x, int y) { int answer = x / y; // Set a breakpoint here return answer; // This will throw an exception if y is 0 } }

Let’s say we wanted to set the value of the variable b by adding x + x, but we made an error and set the value by subtracting x - x, as in the code above. This would lead to the variable b holding a value of zero. This value is set as the value for the y argument in the divide method, leading to a division-by-zero error.

Stack trace

The divide() function will throw an ArithmeticException when trying to divide by zero. Ideally, we would want to add an exception handler to handle this case, but for the purpose of debugging, we will focus on identifying the issue and fixing it temporarily.

Notice the program execution stack trace shows us the line where the error occurred in our code. We can then set a breakpoint on this line to start debugging.

Let’s debug this example in Eclipse:

1. Set a breakpoint: Click in the left margin of the code editor to set a breakpoint. A breakpoint is a marker that tells the debugger to pause the program at that specific line. In the above code, set a breakpoint on the line int answer = x / y;. This will allow you to pause the program before the division by zero occurs.

Setting a breakpoint

2. Start the program in debug mode: Click the bug icon to start the program in debug mode. This allows you to run the code up to the breakpoint and inspect the state of your program.

Share

Share on Twitter
Share on Bluesky
Share on HackerNews
Share on LinkedIn

Published

Sentry Sign Up CTA

Code breaks, fix it faster

Sign up for Sentry and monitor your application in minutes.

Try Sentry Free

Topics

Debugging

New product releases and exclusive demos

Listen to the Syntax Podcast

Of course we sponsor a developer podcast. Check it out on your favorite listening platform.

Listen To Syntax
© 2025 • Sentry is a registered Trademark of Functional Software, Inc.