Step-by-step understanding of Java exception handling

Introduction

The problem of exception handling is a frequent issue in Java code reviews. As mentioned in this article, the basic rules to follow in Java exception handling are not that complicated. Unfortunately, even Java programmers with many years of experience in their work experience are often unable to implement proper exception handling. Not to mention for Java programmers with less experience.

Why is improper exception handling so widespread? There are three major factors that can be considered. First, there is historical confusion in the exception mechanism (especially checked exceptions) in the Java language specification, giving programmers excessive freedom. Secondly, unless you not only develop the application but also actually operate it, you should not notice the harmful effects of inappropriate exception handling. Finally, there is no compact collection of resources to learn proper exception handling. In beginner's books, ad hoc exception handling that causes ʻe.printStackTrace () `is conspicuous, and appropriate exception handling methods are scattered in books and framework documents for intermediate users and above.

Therefore, in this article, we will divide it into three stages, for beginners, beginners, and intermediates, how to avoid the trap of excessive freedom of exception handling, and what is appropriate exception handling from the viewpoint of operation. I would like to make it compact. The target application is mainly intended for the server side (although many items should be applicable not only to the server side).

For beginners

Here are some things to understand before you go to the scene.

Understand the characteristics of the exception mechanism

Representation of anomalous system

There is a big problem with this code, which expresses an abnormal system of method calls with a special return value. First, the caller of createItem () may forget to implement anomalous processing. Also, the caller does not know what was actually wrong, just knowing that the return value is empty.

Item item = createItem();
if (item == null) {
    //Abnormal system
    ...
} else {
    //Normal system
    ...
}

With the introduction of the exception mechanism, the code can be rewritten as follows: As a result, the caller can be guided to explicitly implement the abnormal processing. In addition, the exception message and stack trace caught in the catch clause allow the caller to know the details of the anomaly.

try {
    Item item = createItem();
    //Normal system
    ...
} catch (ItemCreationException e) {
    //Abnormal system
    ...
}

Support for multi-stage call hierarchy

If the exception mechanism is used, it is possible to simply deal with abnormal systems even in a multi-stage call hierarchy. For example, applications with a typical layered architecture tend to have this multi-tiered call hierarchy (at the beginner level, you don't have to worry about the details if you find something complicated).

-(Presentation layer) Pre-processing and post-processing of web framework -(Presentation layer) Controller entry method -(Presentation layer) Controller private method -(Domain layer) Facade-like Service method -(Domain layer) Service method that is responsible for the actual business logic -(Data Source layer) Repository method responsible for data access logic

The problem here is how to convey the abnormal system in the lower layer to the upper layer. For example, suppose that when an error occurs in the data access of the lowest layer, it is necessary to return an appropriate error response in the post-processing of the highest layer. To meet this requirement with a special return value, an abnormal return value must be returned in a bucket-relay manner from the lowest layer to the highest layer.

On the other hand, by using an exception mechanism instead of a special return value, it is possible to handle an abnormality in any lower layer in any upper layer while keeping the input / output of the method simple. The lower layer just throws an exception like throw new IllegalStateException ("Something is wrong") without thinking, and some upper layer just catches the exception in the catch clause.

Understand the difference between checked and unchecked exceptions

Exception class hierarchy

Below is a basic exception class inheritance tree (note that all packages are java.lang). Exceptions that inherit from ʻExceptionare __checked exceptions__, and exceptions that inherit fromRuntimeExceptionare __unchecked exceptions__. Note thatThrowable and ʻError do not require a detailed understanding at the beginner level (rather, it is better not to touch them).

Checked exception

Checked exceptions are exceptions that force the caller to take some action. For example, if you write the code that reads the contents of the file as shown below, a compile error will be output.

List<String> lines = Files.readAllLines(Paths.get("items.csv"));
Error:(x, x) java:Exception java.io.No IOException is reported. Must be captured or declared to throw

Here, there are two steps you can take to get the compilation through from a beginner's point of view. First, there is a method of catching with try-catch. Note that there are many traps in the processing after capture here (appropriate measures will be described later).

try {
    List<String> lines = Files.readAllLines(Paths.get("items.csv"));
    //Normal system
    ...
} catch (IOException e) {
    //Abnormal system
    ...
}

Alternatively, you can explicitly leave it to the caller in the throws clause of the method. However, there are not many situations where this method can be used. Details will be described in the section for intermediate users.

public void processItems() throws IOException {
    List<String> lines = Files.readAllLines(Paths.get("items.csv"));
    //Normal system
    ...
}

Unchecked exception

An unchecked exception is an exception, such as a checked exception, that the caller is not forced to handle. For example, the ʻInteger.parseInt (String) method may throw an unchecked exception NumberFormatException` (reference: Javadoc /api/java/lang/Integer.html#parseInt-java.lang.String-)), but no compilation error is output. Instead, an error occurs at run time.

Integer value = Integer.parseInt("ABC"); //Compile without doing anything
java.lang.NumberFormatException: For input string: "ABC"

Overcome inspection exceptions

Wrap in RuntimeException and send

Now, remember what to do if you catch a checked exception for compilation, just how to wrap it in this RuntimeException and send it at the beginner level. You don't need to understand in detail what happens to the unchecked exceptions you throw at the beginner level. Don't do anything extra.

try {
    List<String> lines = Files.readAllLines(Paths.get("items.csv"));
    //Normal system
    ...
} catch (IOException e) {
    throw new RuntimeException(e); //Wrap in unchecked exception and send
}

Wrap and send in more appropriate unchecked exceptions

RuntimeException It would be better if you could wrap it in an appropriate unchecked exception and send it instead of just focusing on it. Below are some commonly used unchecked exceptions and their very rough uses.

So, in fact, the code wrapped in the above RuntimeException should be rewritten to use ʻUncheckedIOException`.

try {
    List<String> lines = Files.readAllLines(Paths.get("items.csv"));
    //Normal system
    ...
} catch (IOException e) {
    throw new UncheckedIOException(e); //Wrap and send in more appropriate unchecked exceptions
}

For beginners

Here, we will describe what you need to understand when developing and operating under the direction of someone in the field.

Know the forbidden hand of exception handling

As mentioned above, in most cases you can write minimally safe code, just remembering to wrap it in an unchecked exception and send it out. But for some reason, Java programmers tend to deviate from the minimum line by doing extra things or forgetting what's important. The following are common improper exception handling. Please do not imitate it.

(Kinjite) Crush

The first forbidden hand is crushing. As a result of squeezing, a problem occurs in the subsequent processing that expected the correct result of the processing that was originally executed. A typical problem is NullPointerException. In general, there are occasions when NullPointerException is treated as a bad guy in Java, but nothing here is that your own code is bad, not NullPointerException.

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    //crush
}
lines.stream().forEach(...); //I get a NullPointerException because lines are still null here

However, there are few cases that should be crushed with will. In that case, it is better to clearly indicate the intention in a log or a comment.

(Kinjite) Output stack trace and log to the extent of regret

Often seen in the primer is the code that calls ʻe.printStackTrace ()in thecatch clause, which is fine, but this is almost always unacceptable in real-world Java applications. I try to print a message with ʻe.printStackTrace (), but if I continue processing, it's not much different from squeezing exceptions. Also, the output destination of ʻe.printStackTrace ()` is generally standard error output, and unlike logs, meta information such as date and time and request ID that is useful for investigating the cause is missing. If you leave such code unattended, you'll be plagued by a mysterious stack trace that doesn't give you clues to solve it during operation.

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    e.printStackTrace(); //Sorry to output the stack trace
}
lines.stream().forEach(...); //I end up getting a NullPointerException here

Even if replacing ʻe.printStackTrace ()above with log output likelog.warn ("Could not read items: "+ e, e)` solves the output meta information problem, It is the same in that if the processing is continued, it is not much different from crushing the exception.

(Kinjite) Log by yourself and then re-send

It is this forbidden hand that a well-mannered beginner tends to do. After the own log is output individually, the log caused by the same problem is output again by the global exception handler described later. These look like two types of exception logs for different issues in operation, causing confusion. In the adult world, being careful is not unconditionally good.

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    log.warn("Could not read items: " + e, e); //The meticulousness backfires
    throw new UncheckedIOException(e);
}
lines.stream().forEach(...);

If you want to include a message, send it as an exception message.

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    throw new UncheckedIOException("Could not read items", e); //Appeal casually
}
lines.stream().forEach(...);

(Kinjite) Throw another exception

There are some mistakes in which you forget the "wrap" of "wrap and send in unchecked exception" and simply throw another unchecked exception. The downside in this case is that you don't know the root cause of the exception during operation. For example, various events such as the fact that the file does not exist and the right to access the file cannot be considered as the cause of the exception that occurs when accessing the file. If you forget to wrap the exception that indicates the root cause and send another exception, failure analysis during operation becomes difficult.

List<String> lines = null;
try {
    lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
    throw new IllegalStateException("Could not read items"); //Information that should have been in the original exception is lost
}
lines.stream().forEach(...);

(Kinjite) Forget to close the resource

When the open processing of a resource such as a file or database connection is executed, the close processing must be performed after that in principle (whether or not the close processing is strictly required is determined by referring to the Javadoc of the open processing method. thing). A common negative effect of forgetting to close is a so-called memory leak. However, in many cases, the problem is not discovered immediately after the release, and it becomes apparent like a time bomb with the subsequent operation. Once discovered, the site will be exhausted by provisional operations such as searching for the criminal and restarting regularly.

try {
    BufferedReader in = Files.newBufferedReader(Paths.get("items.csv"));
    in.lines().forEach(...);
    //in is not closed
} catch (IOException e) {
    throw new UncheckedIOException(e);
}

Use the try-with-resources syntax instead.

try (BufferedReader in = Files.newBufferedReader(Paths.get("items.csv"))) {
    in.lines().forEach(...);
} catch (IOException e) {
    throw new UncheckedIOException(e);
}

Before the introduction of the try-with-resources syntax, closing processing was performed in the finally clause, but it is redundant and difficult to read, and there are many complicated things to consider, such as what to do if an exception occurs in the finally clause. It is safe to avoid it at the beginner's stage.

BufferedReader in = null;
try {
    in = Files.newBufferedReader(Paths.get("items.csv"));
    in.lines().forEach(...);
} catch (IOException e) {
    throw new UncheckedIOException(e);
} finally {
    try {
        in.close();
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

For reference, try-with-resources can only be used if the target class implements the java.io.Closeable interface or the java.lang.AutoCloseable interface. For details, refer to Official Documents.

(Kinjite) Repeat the same process for multiple exceptions

This forbidden person has almost no operational harm, but it's still bad that the code isn't readable.

try {
    Item item = createItem();
    //Normal system
    ...
} catch (ItemCreationException e) {
    throw new IllegalStateException(e);
} catch (InvalidProcessingException e) {
    throw new IllegalStateException(e);
}

Use multi-catch instead.

try {
    Item item = createItem();
    //Normal system
    ...
} catch (ItemCreationException | InvalidProcessingException e) {
    throw new IllegalStateException(e);
}

Defensive use of unchecked exceptions

Unchecked exceptions can also be used to defensively express conditions such as "only these parameters should come here" and "this process should not be executed".

A common application is the throwing of ʻIllegalArgumentException` for invalid parameters in the guard clause at the beginning of the method.

public void processItem(Item item) {
    if (!item.isValid()) {
        //Invalid parameter
        throw new IllegalArgumentException("Invalid item: " + item);
    }
    //Normal system
    ...
}

The above code is as follows using Preconditions of Guava You can write in one line. I can't say it out loud, but if you don't like the "just in case" level branching that reduces unit test coverage, this is a good option.

public void processItem(Item item) {
    Preconditions.checkArgument(item.isValid(), "Invalid item: %s", item);
    //Normal system
    ...
}

Similarly, when checking for null parameters, it is better to use the standard class library java.util.Objects.requireNonNull (T) instead of your own if statement. By the way, the exception thrown here is the infamous NullPointerException. Guava's similar feature, Preconditions.checkNotNull (T), also throws a NullPointerException, albeit with some resistance. Let's wrap it around a long object.

public void processItem(Item item) {
    Objects.requireNonNull(item, "Item is null");
    //Normal system
    ...
}

In addition, it may throw an exception in the normally unreachable default part of a switch statement.

Result result = processItem(item);
switch (result) {
    case SUCCEEDED:
        ...
        break;
    case FAILED:
        ...
        break;
    ...
    default:
        throw new IllegalStateException("This would not happen");
}

Respond to exceptions globally

So far, the description has focused mainly on the side that throws the exception, but next we will describe how to handle the thrown exception.

(Almost forbidden) Implement your own try-catch process at the top of the call hierarchy

The simplest and most straightforward way to catch exceptions is to write your own last bastion of try-catch processing at the top of the call hierarchy (main method for command line applications and Presentation layer Controller for web applications). However, in reality, there are few opportunities to use this method. In real-world Java application development, there are quite a few cases where the main method is implemented in full scratch, and there is a better alternative to the web framework for the Presentation layer Controller.

Use the exception handling mechanism of the web framework

Modern web frameworks provide a mechanism to execute exception handlers according to the exception class that occurs. The advantage of such a mechanism is that the common processing of multiple Controllers can be flexibly combined without using the inheritance structure of the Controller class. The exception handling mechanism in a typical Web framework is shown below.

-JAX-RS (Jersey) Exception Handling Mechanism --For the exception handler class, the exception class is specified using the type parameter. -Spring MVC Exception Handling Mechanism --Annotations are used to specify exception classes for exception handler classes and exception handler methods.

Utilize the Interceptor mechanism of the DI container

A more general mechanism is the DI container Interceptor mechanism. You can use the Interceptor mechanism to insert pre-processing and post-processing for any method call of a class under the control of a DI container. The Interceptor mechanism in a typical DI container is shown below.

-CDI (Weld) Interceptor Mechanism -Spring Interceptor mechanism

Transaction control (automatic rollback when an exception occurs under a specific method) is a typical process realized by using the Interceptor mechanism for exceptions. The transaction control mechanism in a typical DI container is shown below.

--CDI Transaction Control Mechanism -Spring transaction control mechanism

Define your own exception

When you can handle exceptions globally according to class, you want to create your own exceptions. For example, you may want to return a personalized error response specific to a particular business logic or external API access logic. To create your own exception, you can inherit the existing exception class and override the constructor.

public class ItemCreationException extends RuntimeException {
	public ItemCreationException() {
	}

	public ItemCreationException(String message) {
		super(message);
	}

	public ItemCreationException(Throwable cause) {
		super(cause);
	}

	public ItemCreationException(String message, Throwable cause) {
		super(message, cause);
	}

	public ItemCreationException(String message, Throwable cause, boolean enableSuppression,
			boolean writableStackTrace) {
		super(message, cause, enableSuppression, writableStackTrace);
	}
}

At first glance, the amount of code looks large, but it's actually generated by the IDE, so the work is minor. For example, in the case of Eclipse, in the new class generation dialog, just specify the superclass name and check the constructor generation.

For intermediate users

Here, we will describe what you should understand when developing and operating in the field at your own discretion.

Output the exception log at an appropriate level and operate

For exceptions handled by the global exception handler, it is necessary to output a log at an appropriate level and perform daily operations. The following is an example of the policy regarding the proper use of levels and operation. This is just an example, and each team has different policies.

However, it is generally difficult to strictly apply the rules immediately after the release, and in reality, the above operations will be developed step by step by cutting the phase.

Respond to individual exceptions

In real-world Java applications, exception handling that is specific to a certain function may be required instead of global exception handling. The following cases are typical examples.

Returns another return value

When it is not appropriate by design for the lower layer to throw an exception, it may catch the exception and return another return value. For example, the following code catches an exception indicating that the record does not exist when accessing the database, and instead returns a return value of ʻOptional`.

public Optional<Item> findByName(String name) {
    try {
        Item item = entityManager.createQuery("...", Item.class).setParameter(1, name).getSingleResult();
        return Optional.of(item);
    } catch (NoResultException e) {
        return Optional.empty();
    }
}

Note that you should not choose a design that returns null instead of Optional. See Twitter search results for "null kill" for more details. ..

Retry

In processing such as an external API call, an exception indicating an accidental communication error may be caught and retry processing may be executed. However, when trying to implement retry processing seriously, there are surprisingly many things to consider such as timeout time setting, retry count setting, backoff algorithm, coping with simultaneous retry occurrence, monitoring of retry occurrence status, and so on. Rather than implementing it yourself, Microprofile Retry and [Spring Retry](https:: It would be safer to use a library like //github.com/spring-projects/spring-retry). In the future, it is expected that such logic will be taken up by Service Mesh mechanisms such as Istio outside the application.

Perform other alternative processing

In addition, alternative processing may be performed according to the requirements. Since the processing content is case by case, details are omitted.

Make good use of checked exceptions

Finally, I will explain how to use the checked exceptions that I have avoided so far.

First, for low-level libraries and frameworks, there are occasions when you should make good use of checked exceptions, as is the case with the java.io package.

Also, for business logic, if you want to force the caller to process abnormal systems, using a check exception is an option. Below are examples of methods that use checked exceptions and methods that return enums. The method using checked exceptions has a simple method signature, and similar processing can be shared by utilizing the exception inheritance structure. On the other hand, in the method of returning with enum, it is easy to understand that the caller handles the return value comprehensively. Which one to choose is on a case-by-case basis.

public void sendNotification(Member member, Notification notification) throws InvalidMemberException {
    //Method using check exception
    ...
}
try {
    sendNotification(member, notification);
    //Normal system
    ...
} catch (InvalidMemberException e) {
    //Alternative processing
    ...
}
public NotificationResult sendNotification(Member member, Notification notification) {
    //Method to return with enum
    ...
}
NotificationResult result = sendNotification(member, notification);
switch (result) {
    case SUCCEEDED:
        //Normal system
        ...
        break;
    case INVALID_MEMBER:
        //Alternative processing
        ...
        break;
    ...
}

in conclusion

So far, we have shown what you need to understand about exception handling in real-world Java applications in three stages: beginners, beginners, and intermediates. We hope this article will reduce unproductive code reviews and painful application operations as much as possible.

Recommended Posts

Step-by-step understanding of Java exception handling
[Java] Practice of exception handling [Exception]
Java exception handling?
[Java] Exception handling
☾ Java / Exception handling
Java exception handling
Java exception handling
[Java] Beginner's understanding of Servlet-②
[Java] Beginner's understanding of Servlet-①
[Java] About try-catch exception handling
Java exception handling usage rules
Exception handling techniques in Java
[In-house study session] Java exception handling (2017/04/26)
Handling of time zones using Java
Exception handling
[Note] Handling of Java decimal point
Step-by-step understanding of O / R mapping
[For Java beginners] About exception handling
Exception handling Exception
Java (exception handling, threading, collection, file IO)
try-catch-finally exception handling How to use java
About exception handling
About exception handling
ruby exception handling
[Java] Exception instance
Ruby exception handling
[Java] Overview of Java
[Java] Handling of JavaBeans in the method chain
Questions in java exception handling throw and try-catch
Expired collection of java
Predicted Features of Java
[Java] Significance of serialVersionUID
About Ruby exception handling
Exception handling practice (ArithmeticException)
NIO.2 review of java
Review of java Shilber
[Ruby] Exception handling basics
[java] throw an exception
java --Unification of comments
Spring Boot exception handling
History of Java annotation
java (merits of polymorphism)
[Java] When writing the source ... A memorandum of understanding ①
"No.1 understanding of this kind of thing" tomcat restart [Java]
[Java Silver] (Exception handling) About try-catch-finally and try-with-resource statements
NIO review of java
[Java] Three features of Java
Summary of Java support 2018
[Java] Handling of character strings (String class and StringBuilder class)
[Swift] Correction of exception handling for vending machines (extra edition)
"Understanding this kind of thing No.2" Volume of comparison operators [Java]
Recommendation of set operation by Java (and understanding of equals and hashCode)
Reintroduction to Java for Humanities 0: Understanding the Act of Programming
[Introduction to Java] Handling of character strings (String class, StringBuilder class)
About the handling of Null
Classes that require exception handling
Java basic learning content 7 (exception)
About an instance of java
Handling of SNMP traps (CentOS 8)
[Java] Mirage-Basic usage of SQL
Java's first exception handling (memories)