How do I handle exceptions and errors in Jakarta Servlets?

Handling exceptions and errors in Jakarta Servlets involves several approaches, ranging from defining error-handling configurations in the deployment descriptor (web.xml) to using annotations and specific coding practices. Below are the typical ways to handle errors and exceptions in Jakarta Servlets:


1. Using web.xml for Declarative Exception Handling

In the web.xml file, you can define error pages that are mapped to specific exceptions or HTTP status codes. When an error or exception occurs, the corresponding error page is displayed.

Example:

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
    <!-- Define an error page for a specific HTTP status code -->
    <error-page>
        <error-code>404</error-code>
        <location>/error-404.jsp</location>
    </error-page>

    <!-- Define an error page for a specific exception -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/error-exception.jsp</location>
    </error-page>
</web-app>
  • error-code: Maps a specific HTTP status code (e.g., 404) to an error page.
  • exception-type: Maps a specific Java exception to an error page.
  • location: Specifies the location of the JSP or HTML page to display.

2. Using @WebServlet and Exception Handling in Code

In your custom servlet, you can explicitly handle exceptions using try-catch blocks or override the service or doGet/doPost methods.

Example:

package org.kodejava.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(urlPatterns = "/exceptionServlet", asyncSupported = true)
public class ExceptionServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            // Servlet logic that may throw an exception
            int result = 10 / 0;  // This will throw an ArithmeticException
        } catch (ArithmeticException e) {
            // Handle specific exceptions
            request.setAttribute("errorMessage", "An error occurred: " + e.getMessage());
            request.getRequestDispatcher("/error.jsp").forward(request, response);
        } catch (Exception e) {
            // Handle generic exceptions
            throw new ServletException("An unexpected error occurred", e);
        }
    }
}

3. Custom Error Pages in JSP

Error pages can be written as JSPs (.jsp). To make use of the exception details, JSPs have access to the exception and other implicit objects such as request and response.

Example: error-exception.jsp

<%@ page isErrorPage="true" %>
<html>
<head><title>Error Page</title></head>
<body>
    <h1>An error occurred</h1>
    <p>Error message: ${exception.message}</p>
    <p>Exception type: ${exception.class.name}</p>
</body>
</html>
  • The attribute isErrorPage="true" makes the JSP aware of the exception object.

4. Defining an Error Filter

You can define a Jakarta Servlet filter that intercepts all requests and handles errors more generically before they reach the targeted servlet or after they are processed.

Example:

package org.kodejava.servlet;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;

import java.io.IOException;

@WebFilter("/*")
public class ErrorHandlingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            chain.doFilter(request, response); // Pass request and response to the next filter or servlet
        } catch (Exception e) {
            request.setAttribute("errorMessage", e.getMessage());
            request.getRequestDispatcher("/error.jsp").forward(request, response);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

This filter catches any unhandled exceptions and forwards the request to an error page.


5. Logging Exceptions

It is a best practice to log exceptions for debugging and monitoring purposes. You can use logging frameworks like Java’s java.util.logging, Log4j, or SLF4J to capture exceptions.

Example with java.util.logging:

package org.kodejava.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.logging.Logger;

@WebServlet(urlPatterns = "/loggingServlet", asyncSupported = true)
public class LoggingServlet extends HttpServlet {
    private static final Logger logger = Logger.getLogger(LoggingServlet.class.getName());

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try {
            // Code that might throw exceptions
        } catch (Exception e) {
            logger.severe("An unexpected error occurred: " + e.getMessage());
            throw new ServletException("Internal server error", e);
        }
    }
}

6. Using HTTP Status Codes

Always set appropriate HTTP status codes when handling errors, so clients are aware of the issue.

Example:

response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid input provided");

Best Practices:

  1. Consistent Error Pages: Create a unified style for error pages to provide a better user experience.
  2. Avoid Revealing Sensitive Data: Never display stack traces or sensitive information to the user.
  3. Use Centralized Logging: Implement a centralized mechanism to log and monitor exceptions.
  4. Handle Specific Errors: Provide user-friendly error messages for known and predictable failures (e.g., 404 Not Found, 403 Forbidden).
  5. Fallback Handling: Ensure a fallback mechanism is in place for uncaught exceptions (e.g., generic error page).

By combining these mechanisms, you can effectively handle exceptions and errors in Jakarta Servlets while ensuring robust error handling and user-friendly responses.


Maven dependencies

<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.1.0</version>
    <scope>provided</scope>
</dependency>

Maven Central

What are the benefits of using parameterized log messages?

When creating a program in Java, we are mostly, if not always, add logging mechanism in our program. This log can be use to add debug information to our program, which can help us when problem occurs in our program. We usually add log message to the start of our methods to show the value of input parameters, or at the end of the methods before exiting to show the process results. We also add log information in the catch block of the try-catch statement to log messages related to exception that occurs in our program.

Here’s an example to illustrate the difference:

Concatenation approach:

logger.info("User " + username + " logged in at " + loginTime);

Parameterized approach:

logger.info("User {} logged in at {}", username, loginTime);

In Java, using parameterized log messages instead of concatenation offers several benefits:

Performance

Parameterized log messages can improve performance compared to string concatenation. When you concatenate strings, the JVM creates a new string object each time, which can lead to unnecessary memory allocation and garbage collection overhead. Parameterized messages, on the other hand, only evaluate the placeholders when the log message is actually logged, which can reduce memory usage and improve performance.

Readability

Parameterized log messages often result in cleaner and more readable code. By separating the log message template from the actual values, it’s easier to understand the intent of the log message and identify the dynamic values being logged.

Internationalization (i18n) and Localization (l10n)

Parameterized log messages make it easier to support internationalization and localization efforts. Since the placeholders in the log message template remain the same regardless of the language, translators can focus solely on translating the template and not worry about the dynamic values.

Prevention of unnecessary string manipulation

When concatenating strings for log messages, you may inadvertently perform unnecessary string manipulation operations (e.g., converting non-string values to strings). With parameterized messages, these operations are only performed if the log message is actually logged, reducing unnecessary computation.

Avoidance of potential formatting issues

When concatenating strings, you may encounter formatting issues, especially if the dynamic values contain special characters or formatting codes. Parameterized messages handle formatting automatically, ensuring that the logged values are properly formatted according to their data types.

Overall, using parameterized log messages can lead to more efficient, readable, and maintainable code in Java logging practices.

Below is another ilustration of adding log messages in our program:

package org.kodejava.util.logging;

import java.util.logging.Level;
import java.util.logging.Logger;

public class ExampleLogger {

    private static final Logger logger = Logger.getLogger(ExampleLogger.class.getName());

    public void performLogin(String username, String loginTime) {
        // Simulate login process
        boolean loginSuccessful = true; // Assume login is successful for demonstration

        if (loginSuccessful) {
            // Log successful login using parameterized message
            logger.log(Level.INFO, "User {0} logged in at {1}", new Object[]{username, loginTime});
        } else {
            // Log failed login using parameterized message
            logger.log(Level.WARNING, "Failed login attempt for user {0} at {1}", new Object[]{username, loginTime});
        }
    }

    public static void main(String[] args) {
        ExampleLogger exampleLogger = new ExampleLogger();
        exampleLogger.performLogin("john_doe", "2024-06-08 10:00:00");
    }
}

In this example:

  • We define a performLogin method that simulates a user login process. The method takes username and loginTime as parameters.
  • Inside the method, we set a boolean variable loginSuccessful to true for demonstration purposes (assuming the login is successful).
  • We then use the java.util.logging.Logger class to log the login event. We use parameterized log messages with placeholders {0} and {1} for username and loginTime, respectively.
  • Depending on whether the login is successful or not, we log the event at different levels (INFO for successful login and WARNING for failed login).
  • In the main method, we create an instance of ExampleLogger and call the performLogin method with sample values for username and loginTime.

This example demonstrates the usage of parameterized log messages in a complete method for logging login events in Java.

How do I create a rolling log files?

In this example we create a rolling or a sequenced of log files. Instead of just limiting the file size (see. How do I limit the size of log file) we can also make the log file to roll. This will prevent a lost to an important log message if we use a single log file.

When using more than one file the log file name will have a sequence number in it starting from 0 to N-1. If we set the count to 5 then we’ll have log files such as myapp.log.0, myapp.log.1 up to myapp.log.5.

If the first log file (myapp.log.0) is about to full, it will be renamed to (myapp.log.1) before the log is written to the first log file. The log is always written to the first file (myapp.log.0).

To read the log messages in sequence you need to start from the highest to the lowest sequence number. By running this program multiple times you’ll see the creation of the log file one by one.

package org.kodejava.util.logging;

import java.util.logging.Logger;
import java.util.logging.FileHandler;
import java.util.logging.SimpleFormatter;
import java.io.IOException;

public class RollingLogFile {
    // Set a small log file size to demonstrate the rolling log files.
    public static final int FILE_SIZE = 1024;

    public static void main(String[] args) {
        Logger logger = Logger.getLogger(RollingLogFile.class.getName());

        try {
            // Creating an instance of FileHandler with 5 logging files
            // sequences.
            FileHandler handler = new FileHandler("myapp.log", FILE_SIZE, 5, true);
            handler.setFormatter(new SimpleFormatter());
            logger.addHandler(handler);
            logger.setUseParentHandlers(false);
        } catch (IOException e) {
            logger.warning("Failed to initialize logger handler.");
        }

        logger.info("Logging information message.");
        logger.warning("Logging warning message.");
    }
}

How do I limit the size of log file?

In this example you learn how to limit the size of a log file when using a FileHandler handler. Limiting the log file will prevent the log file to grow wildly without limit.

package org.kodejava.util.logging;

import java.util.logging.Logger;
import java.util.logging.FileHandler;
import java.io.IOException;

public class LogFileLimit {
    // The log file size is set to 1 MB.
    public static final int FILE_SIZE = 1024 * 1024;

    public static void main(String[] args) {
        Logger logger = Logger.getLogger(LogFileLimit.class.getName());

        try {
            // Create a FileHandler with 1 MB file size and a single log file.
            // We also tell the handler to append the log message.
            FileHandler handler = new FileHandler("myapp.log", FILE_SIZE, 1, true);
            logger.addHandler(handler);
        } catch (IOException e) {
            logger.warning("Failed to initialize logger handler.");
        }

        logger.info("Test info");
        logger.warning("Test warning");
        logger.severe("Test severe");
    }
}

How do I create a custom logger Formatter?

To create a custom Formatter we need to extend the java.util.logging.Formatter abstract class and implements the format(LogRecord) method. In the method then we can format the log message stored in the LogRecord to match our need.

The java.util.logging.Formatter class also have the getHead(Handler) and getTail(Handler) which can be overridden to add a head and a tail to our log message.

package org.kodejava.util.logging;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.*;

public class LogCustomFormatter {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(LogCustomFormatter.class.getName());
        logger.setUseParentHandlers(false);

        MyFormatter formatter = new MyFormatter();
        ConsoleHandler handler = new ConsoleHandler();
        handler.setFormatter(formatter);

        logger.addHandler(handler);
        logger.info("Example of creating custom formatter.");
        logger.warning("A warning message.");
        logger.severe("A severe message.");
    }
}

class MyFormatter extends Formatter {
    // Create a DateFormat to format the logger timestamp.
    private static final DateFormat df = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss.SSS");

    public String format(LogRecord record) {
        StringBuilder builder = new StringBuilder(1000);
        builder.append(df.format(new Date(record.getMillis()))).append(" - ");
        builder.append("[").append(record.getSourceClassName()).append(".");
        builder.append(record.getSourceMethodName()).append("] - ");
        builder.append("[").append(record.getLevel()).append("] - ");
        builder.append(formatMessage(record));
        builder.append("\n");
        return builder.toString();
    }

    public String getHead(Handler h) {
        return super.getHead(h);
    }

    public String getTail(Handler h) {
        return super.getTail(h);
    }
}

Below is an output produced by the custom formatter above.

08/10/2021 07:55:55.153 - [org.kodejava.util.logging.LogCustomFormatter.main] - [INFO] - Example of creating custom formatter.
08/10/2021 07:55:55.164 - [org.kodejava.util.logging.LogCustomFormatter.main] - [WARNING] - A warning message.
08/10/2021 07:55:55.164 - [org.kodejava.util.logging.LogCustomFormatter.main] - [SEVERE] - A severe message.